1use crate::workspace::{EntityId, Workspace};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json;
10use serde_yaml;
11use std::collections::HashMap;
12use std::path::Path;
13use tokio::fs;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SyncConfig {
18 pub enabled: bool,
20 pub provider: SyncProvider,
22 pub interval_seconds: u64,
24 pub conflict_strategy: ConflictResolutionStrategy,
26 pub auto_commit: bool,
28 pub auto_push: bool,
30 pub directory_structure: SyncDirectoryStructure,
32 pub sync_direction: SyncDirection,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub enum SyncProvider {
39 Git {
41 repo_url: String,
43 branch: String,
45 auth_token: Option<String>,
47 },
48 Cloud {
50 service_url: String,
52 api_key: String,
54 project_id: String,
56 },
57 Local {
59 directory_path: String,
61 watch_changes: bool,
63 },
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum ConflictResolutionStrategy {
69 LocalWins,
71 RemoteWins,
73 Manual,
75 LastModified,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum SyncDirectoryStructure {
82 SingleDirectory,
84 PerWorkspace,
86 Hierarchical,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum SyncDirection {
93 Bidirectional,
95 LocalToRemote,
97 RemoteToLocal,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SyncStatus {
104 pub last_sync: Option<DateTime<Utc>>,
106 pub state: SyncState,
108 pub pending_changes: usize,
110 pub conflicts: usize,
112 pub last_error: Option<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub enum SyncState {
119 NotSynced,
121 Syncing,
123 Synced,
125 SyncFailed,
127 HasConflicts,
129}
130
131#[derive(Debug, Clone)]
133pub struct SyncResult {
134 pub success: bool,
136 pub changes_count: usize,
138 pub conflicts: Vec<SyncConflict>,
140 pub error: Option<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct SyncConflict {
147 pub entity_id: EntityId,
149 pub entity_type: String,
151 pub local_version: serde_json::Value,
153 pub remote_version: serde_json::Value,
155 pub resolution: ConflictResolution,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum ConflictResolution {
162 Local,
164 Remote,
166 Manual,
168}
169
170#[derive(Debug, Clone)]
172pub struct WorkspaceSyncManager {
173 config: SyncConfig,
175 status: SyncStatus,
177 conflicts: Vec<SyncConflict>,
179 total_syncs: usize,
181 successful_syncs: usize,
183 failed_syncs: usize,
185 resolved_conflicts: usize,
187 last_sync_duration_ms: Option<u64>,
189}
190
191#[derive(Debug, Clone)]
193pub enum SyncEvent {
194 Started,
196 Progress {
198 current: usize,
200 total: usize,
202 },
203 Completed(SyncResult),
205 Failed(String),
207 ConflictDetected(SyncConflict),
209}
210
211impl WorkspaceSyncManager {
212 pub fn new(config: SyncConfig) -> Self {
214 let status = SyncStatus {
215 last_sync: None,
216 state: SyncState::NotSynced,
217 pending_changes: 0,
218 conflicts: 0,
219 last_error: None,
220 };
221
222 Self {
223 config,
224 status,
225 conflicts: Vec::new(),
226 total_syncs: 0,
227 successful_syncs: 0,
228 failed_syncs: 0,
229 resolved_conflicts: 0,
230 last_sync_duration_ms: None,
231 }
232 }
233
234 pub fn get_config(&self) -> &SyncConfig {
236 &self.config
237 }
238
239 pub fn update_config(&mut self, config: SyncConfig) {
241 self.config = config;
242 }
243
244 pub fn get_status(&self) -> &SyncStatus {
246 &self.status
247 }
248
249 pub fn get_conflicts(&self) -> &[SyncConflict] {
251 &self.conflicts
252 }
253
254 pub fn is_enabled(&self) -> bool {
256 self.config.enabled
257 }
258
259 pub async fn sync_workspace(
261 &mut self,
262 workspace: &mut Workspace,
263 ) -> Result<SyncResult, String> {
264 if !self.config.enabled {
265 return Err("Synchronization is disabled".to_string());
266 }
267
268 self.total_syncs += 1;
270
271 let start_time = std::time::Instant::now();
273
274 self.status.state = SyncState::Syncing;
275 self.status.last_error = None;
276
277 let result = match &self.config.provider {
278 SyncProvider::Git {
279 repo_url,
280 branch,
281 auth_token,
282 } => self.sync_with_git(workspace, repo_url, branch, auth_token.as_deref()).await,
283 SyncProvider::Cloud {
284 service_url,
285 api_key,
286 project_id,
287 } => self.sync_with_cloud(workspace, service_url, api_key, project_id).await,
288 SyncProvider::Local {
289 directory_path,
290 watch_changes,
291 } => self.sync_with_local(workspace, directory_path, *watch_changes).await,
292 };
293
294 let duration = start_time.elapsed();
296 let duration_ms = duration.as_millis() as u64;
297 self.last_sync_duration_ms = Some(duration_ms);
298
299 match &result {
300 Ok(sync_result) => {
301 if sync_result.success {
302 self.successful_syncs += 1;
303 self.status.state = SyncState::Synced;
304 self.status.last_sync = Some(Utc::now());
305 self.status.pending_changes = 0;
306 self.status.conflicts = sync_result.conflicts.len();
307 } else {
308 self.failed_syncs += 1;
309 self.status.state = SyncState::SyncFailed;
310 self.status.last_error = sync_result.error.clone();
311 }
312 }
313 Err(error) => {
314 self.failed_syncs += 1;
315 self.status.state = SyncState::SyncFailed;
316 self.status.last_error = Some(error.clone());
317 }
318 }
319
320 result
321 }
322
323 async fn sync_with_git(
325 &self,
326 workspace: &mut Workspace,
327 repo_url: &str,
328 branch: &str,
329 auth_token: Option<&str>,
330 ) -> Result<SyncResult, String> {
331 let temp_dir =
333 tempfile::tempdir().map_err(|e| format!("Failed to create temp directory: {}", e))?;
334
335 let repo_path = temp_dir.path().join("repo");
336
337 match self.config.sync_direction {
338 SyncDirection::LocalToRemote => {
339 self.sync_local_to_git(workspace, repo_url, branch, auth_token, &repo_path)
340 .await
341 }
342 SyncDirection::RemoteToLocal => {
343 self.sync_git_to_local(workspace, repo_url, branch, auth_token, &repo_path)
344 .await
345 }
346 SyncDirection::Bidirectional => {
347 self.sync_bidirectional_git(workspace, repo_url, branch, auth_token, &repo_path)
348 .await
349 }
350 }
351 }
352
353 async fn sync_local_to_git(
355 &self,
356 workspace: &Workspace,
357 repo_url: &str,
358 branch: &str,
359 auth_token: Option<&str>,
360 repo_path: &std::path::Path,
361 ) -> Result<SyncResult, String> {
362 self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
364
365 let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
367 let workspace_yaml = serde_yaml::to_string(workspace)
368 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
369
370 tokio::fs::write(&workspace_file, &workspace_yaml)
371 .await
372 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
373
374 self.git_add_commit_push(repo_path, &workspace_file, auth_token).await?;
376
377 Ok(SyncResult {
378 success: true,
379 changes_count: 1,
380 conflicts: vec![],
381 error: None,
382 })
383 }
384
385 async fn sync_git_to_local(
387 &self,
388 workspace: &mut Workspace,
389 repo_url: &str,
390 branch: &str,
391 auth_token: Option<&str>,
392 repo_path: &std::path::Path,
393 ) -> Result<SyncResult, String> {
394 self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
396
397 let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
399
400 if !workspace_file.exists() {
401 return Ok(SyncResult {
402 success: true,
403 changes_count: 0,
404 conflicts: vec![],
405 error: None,
406 });
407 }
408
409 let workspace_yaml = tokio::fs::read_to_string(&workspace_file)
410 .await
411 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
412
413 let remote_workspace: Workspace = serde_yaml::from_str(&workspace_yaml)
414 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
415
416 let conflicts = self.detect_conflicts(workspace, &remote_workspace);
418
419 if conflicts.is_empty() {
420 *workspace = remote_workspace;
422 Ok(SyncResult {
423 success: true,
424 changes_count: 1,
425 conflicts: vec![],
426 error: None,
427 })
428 } else {
429 Ok(SyncResult {
431 success: true,
432 changes_count: 0,
433 conflicts,
434 error: None,
435 })
436 }
437 }
438
439 async fn sync_bidirectional_git(
441 &self,
442 workspace: &mut Workspace,
443 repo_url: &str,
444 branch: &str,
445 auth_token: Option<&str>,
446 repo_path: &std::path::Path,
447 ) -> Result<SyncResult, String> {
448 let pull_result = self
450 .sync_git_to_local(workspace, repo_url, branch, auth_token, repo_path)
451 .await?;
452
453 if !pull_result.conflicts.is_empty() {
454 return Ok(pull_result);
456 }
457
458 self.sync_local_to_git(workspace, repo_url, branch, auth_token, repo_path).await
460 }
461
462 async fn ensure_git_repo(
464 &self,
465 repo_url: &str,
466 branch: &str,
467 auth_token: Option<&str>,
468 repo_path: &std::path::Path,
469 ) -> Result<(), String> {
470 use std::process::Command;
471
472 let repo_path_str = repo_path.to_string_lossy();
474
475 if repo_path.exists() {
476 let output = Command::new("git")
478 .args(["-C", repo_path_str.as_ref(), "pull", "origin", branch])
479 .output()
480 .map_err(|e| format!("Failed to pull repository: {}", e))?;
481
482 if !output.status.success() {
483 let stderr = String::from_utf8_lossy(&output.stderr);
484 return Err(format!("Git pull failed: {}", stderr));
485 }
486 } else {
487 let clone_url = if let Some(token) = auth_token {
490 self.inject_auth_token_into_url(repo_url, token)
491 } else {
492 repo_url.to_string()
493 };
494
495 let output = Command::new("git")
496 .args([
497 "clone",
498 "--branch",
499 branch,
500 &clone_url,
501 repo_path_str.as_ref(),
502 ])
503 .output()
504 .map_err(|e| format!("Failed to clone repository: {}", e))?;
505
506 if !output.status.success() {
507 let stderr = String::from_utf8_lossy(&output.stderr);
508 return Err(format!("Git clone failed: {}", stderr));
509 }
510 }
511
512 Ok(())
513 }
514
515 async fn git_add_commit_push(
517 &self,
518 repo_path: &std::path::Path,
519 workspace_file: &std::path::Path,
520 _auth_token: Option<&str>,
521 ) -> Result<(), String> {
522 use std::process::Command;
523
524 let repo_path_str = repo_path.to_string_lossy();
526
527 let file_path_str = workspace_file
529 .strip_prefix(repo_path)
530 .unwrap_or(workspace_file)
531 .to_string_lossy();
532
533 let output = Command::new("git")
535 .args(["-C", repo_path_str.as_ref(), "add", file_path_str.as_ref()])
536 .output()
537 .map_err(|e| format!("Failed to add file to git: {}", e))?;
538
539 if !output.status.success() {
540 let stderr = String::from_utf8_lossy(&output.stderr);
541 return Err(format!("Git add failed: {}", stderr));
542 }
543
544 let status_output = Command::new("git")
546 .args(["-C", repo_path_str.as_ref(), "status", "--porcelain"])
547 .output()
548 .map_err(|e| format!("Failed to check git status: {}", e))?;
549
550 if status_output.stdout.is_empty() {
551 return Ok(());
553 }
554
555 let output = Command::new("git")
557 .args([
558 "-C",
559 repo_path_str.as_ref(),
560 "commit",
561 "-m",
562 "Update workspace",
563 ])
564 .output()
565 .map_err(|e| format!("Failed to commit changes: {}", e))?;
566
567 if !output.status.success() {
568 let stderr = String::from_utf8_lossy(&output.stderr);
569 return Err(format!("Git commit failed: {}", stderr));
570 }
571
572 let output = Command::new("git")
574 .args(["-C", repo_path_str.as_ref(), "push", "origin", "HEAD"])
575 .output()
576 .map_err(|e| format!("Failed to push changes: {}", e))?;
577
578 if !output.status.success() {
579 let stderr = String::from_utf8_lossy(&output.stderr);
580 return Err(format!("Git push failed: {}", stderr));
581 }
582
583 Ok(())
584 }
585
586 fn inject_auth_token_into_url(&self, url: &str, token: &str) -> String {
588 if let Some(https_pos) = url.find("https://") {
589 let rest = &url[https_pos + "https://".len()..];
590 format!("https://oauth2:{}@{}", token, rest)
591 } else {
592 url.to_string()
594 }
595 }
596
597 async fn sync_with_cloud(
599 &self,
600 workspace: &mut Workspace,
601 service_url: &str,
602 api_key: &str,
603 project_id: &str,
604 ) -> Result<SyncResult, String> {
605 let client = reqwest::Client::new();
607
608 let base_url = service_url.trim_end_matches('/');
610 let workspace_url =
611 format!("{}/api/v1/projects/{}/workspaces/{}", base_url, project_id, workspace.id);
612
613 match self.config.sync_direction {
614 SyncDirection::LocalToRemote => {
615 self.upload_workspace_to_cloud(&client, &workspace_url, api_key, workspace)
617 .await
618 }
619 SyncDirection::RemoteToLocal => {
620 self.download_workspace_from_cloud(&client, &workspace_url, api_key, workspace)
622 .await
623 }
624 SyncDirection::Bidirectional => {
625 self.bidirectional_sync(&client, &workspace_url, api_key, workspace).await
627 }
628 }
629 }
630
631 async fn upload_workspace_to_cloud(
633 &self,
634 client: &reqwest::Client,
635 workspace_url: &str,
636 api_key: &str,
637 workspace: &Workspace,
638 ) -> Result<SyncResult, String> {
639 let workspace_json = serde_json::to_string(workspace)
641 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
642
643 let response = client
645 .put(workspace_url)
646 .header("Authorization", format!("Bearer {}", api_key))
647 .header("Content-Type", "application/json")
648 .body(workspace_json)
649 .send()
650 .await
651 .map_err(|e| format!("Failed to upload workspace: {}", e))?;
652
653 if !response.status().is_success() {
654 let status = response.status();
655 let error_text = response.text().await.unwrap_or_default();
656 return Err(format!("Cloud upload failed with status {}: {}", status, error_text));
657 }
658
659 Ok(SyncResult {
660 success: true,
661 changes_count: 1,
662 conflicts: vec![],
663 error: None,
664 })
665 }
666
667 async fn download_workspace_from_cloud(
669 &self,
670 client: &reqwest::Client,
671 workspace_url: &str,
672 api_key: &str,
673 workspace: &mut Workspace,
674 ) -> Result<SyncResult, String> {
675 let response = client
677 .get(workspace_url)
678 .header("Authorization", format!("Bearer {}", api_key))
679 .send()
680 .await
681 .map_err(|e| format!("Failed to download workspace: {}", e))?;
682
683 if response.status() == reqwest::StatusCode::NOT_FOUND {
684 return Ok(SyncResult {
686 success: true,
687 changes_count: 0,
688 conflicts: vec![],
689 error: None,
690 });
691 }
692
693 if !response.status().is_success() {
694 let status = response.status();
695 let error_text = response.text().await.unwrap_or_default();
696 return Err(format!("Cloud download failed with status {}: {}", status, error_text));
697 }
698
699 let remote_json: serde_json::Value = response
700 .json()
701 .await
702 .map_err(|e| format!("Failed to parse remote workspace: {}", e))?;
703
704 let remote_workspace: Workspace = serde_json::from_value(remote_json.clone())
706 .map_err(|e| format!("Failed to deserialize remote workspace: {}", e))?;
707
708 let conflicts = self.detect_conflicts(workspace, &remote_workspace);
710
711 if conflicts.is_empty() {
713 *workspace = remote_workspace;
715 Ok(SyncResult {
716 success: true,
717 changes_count: 1,
718 conflicts: vec![],
719 error: None,
720 })
721 } else {
722 Ok(SyncResult {
724 success: true,
725 changes_count: 0,
726 conflicts,
727 error: None,
728 })
729 }
730 }
731
732 async fn bidirectional_sync(
734 &self,
735 client: &reqwest::Client,
736 workspace_url: &str,
737 api_key: &str,
738 workspace: &mut Workspace,
739 ) -> Result<SyncResult, String> {
740 let download_result = self
742 .download_workspace_from_cloud(client, workspace_url, api_key, workspace)
743 .await?;
744
745 if !download_result.conflicts.is_empty() {
746 return Ok(download_result);
748 }
749
750 self.upload_workspace_to_cloud(client, workspace_url, api_key, workspace).await
752 }
753
754 fn detect_conflicts(&self, local: &Workspace, remote: &Workspace) -> Vec<SyncConflict> {
756 let mut conflicts = vec![];
757
758 if local.updated_at > remote.updated_at {
760 let local_json = serde_json::to_value(local).unwrap_or_default();
762 let remote_json = serde_json::to_value(remote).unwrap_or_default();
763
764 if local_json != remote_json {
765 conflicts.push(SyncConflict {
766 entity_id: local.id.clone(),
767 entity_type: "workspace".to_string(),
768 local_version: local_json,
769 remote_version: remote_json,
770 resolution: ConflictResolution::Manual,
771 });
772 }
773 }
774
775 conflicts
776 }
777
778 async fn sync_with_local(
780 &self,
781 workspace: &mut Workspace,
782 directory_path: &str,
783 _watch_changes: bool,
784 ) -> Result<SyncResult, String> {
785 let dir_path = Path::new(directory_path);
786
787 if !dir_path.exists() {
789 fs::create_dir_all(dir_path)
790 .await
791 .map_err(|e| format!("Failed to create directory {}: {}", directory_path, e))?;
792 }
793
794 match self.config.sync_direction {
795 SyncDirection::LocalToRemote => {
796 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
798 let content = serde_yaml::to_string(workspace)
799 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
800
801 fs::write(&file_path, content)
802 .await
803 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
804
805 Ok(SyncResult {
806 success: true,
807 changes_count: 1,
808 conflicts: vec![],
809 error: None,
810 })
811 }
812 SyncDirection::RemoteToLocal => {
813 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
815
816 if !file_path.exists() {
817 return Err(format!("Workspace file not found: {:?}", file_path));
818 }
819
820 let content = fs::read_to_string(&file_path)
821 .await
822 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
823
824 let remote_workspace: Workspace = serde_yaml::from_str(&content)
825 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
826
827 let conflicts = {
829 let mut conflicts = vec![];
830
831 if workspace.updated_at > remote_workspace.updated_at {
833 let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
835 let remote_json =
836 serde_json::to_value(&remote_workspace).unwrap_or_default();
837 conflicts.push(SyncConflict {
838 entity_id: workspace.id.clone(),
839 entity_type: "workspace".to_string(),
840 local_version: local_json,
841 remote_version: remote_json,
842 resolution: ConflictResolution::Manual,
843 });
844 } else if workspace.updated_at == remote_workspace.updated_at {
845 let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
847 let remote_json =
848 serde_json::to_value(&remote_workspace).unwrap_or_default();
849 if local_json != remote_json {
850 conflicts.push(SyncConflict {
852 entity_id: workspace.id.clone(),
853 entity_type: "workspace".to_string(),
854 local_version: local_json,
855 remote_version: remote_json,
856 resolution: ConflictResolution::Manual,
857 });
858 }
859 }
860
861 conflicts
862 };
863
864 if conflicts.is_empty() && remote_workspace.updated_at >= workspace.updated_at {
866 *workspace = remote_workspace;
867 Ok(SyncResult {
868 success: true,
869 changes_count: 1,
870 conflicts: vec![],
871 error: None,
872 })
873 } else {
874 Ok(SyncResult {
875 success: true,
876 changes_count: 0,
877 conflicts,
878 error: None,
879 })
880 }
881 }
882 SyncDirection::Bidirectional => {
883 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
885
886 let mut conflicts = vec![];
887
888 if file_path.exists() {
889 let content = fs::read_to_string(&file_path)
890 .await
891 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
892
893 let remote_workspace: Workspace = serde_yaml::from_str(&content)
894 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
895
896 if remote_workspace.updated_at > workspace.updated_at {
898 let remote_version =
900 serde_json::to_value(&remote_workspace).unwrap_or_default();
901 conflicts.push(SyncConflict {
902 entity_id: workspace.id.clone(),
903 entity_type: "workspace".to_string(),
904 local_version: serde_json::to_value(&*workspace).unwrap_or_default(),
905 remote_version,
906 resolution: ConflictResolution::Manual,
907 });
908 }
909 }
910
911 let content = serde_yaml::to_string(workspace)
913 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
914
915 fs::write(&file_path, content)
916 .await
917 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
918
919 Ok(SyncResult {
920 success: true,
921 changes_count: 1,
922 conflicts,
923 error: None,
924 })
925 }
926 }
927 }
928
929 pub fn resolve_conflicts(
931 &mut self,
932 resolutions: HashMap<EntityId, ConflictResolution>,
933 ) -> Result<usize, String> {
934 let mut resolved_count = 0;
935
936 for conflict in &self.conflicts.clone() {
937 if let Some(resolution) = resolutions.get(&conflict.entity_id) {
938 match resolution {
939 ConflictResolution::Local => {
940 resolved_count += 1;
942 }
943 ConflictResolution::Remote => {
944 resolved_count += 1;
946 }
947 ConflictResolution::Manual => {
948 continue;
950 }
951 }
952 }
953 }
954
955 self.resolved_conflicts += resolved_count;
957
958 self.conflicts.retain(|conflict| {
960 !resolutions.contains_key(&conflict.entity_id)
961 || matches!(resolutions.get(&conflict.entity_id), Some(ConflictResolution::Manual))
962 });
963
964 self.status.conflicts = self.conflicts.len();
965 if self.conflicts.is_empty() {
966 self.status.state = SyncState::Synced;
967 } else {
968 self.status.state = SyncState::HasConflicts;
969 }
970
971 Ok(resolved_count)
972 }
973
974 pub fn get_sync_stats(&self) -> SyncStats {
976 SyncStats {
977 total_syncs: self.total_syncs,
978 successful_syncs: self.successful_syncs,
979 failed_syncs: self.failed_syncs,
980 total_conflicts: self.conflicts.len(),
981 resolved_conflicts: self.resolved_conflicts,
982 last_sync_duration_ms: self.last_sync_duration_ms,
983 }
984 }
985
986 pub fn export_config(&self) -> Result<String, String> {
988 serde_json::to_string_pretty(&self.config)
989 .map_err(|e| format!("Failed to serialize sync config: {}", e))
990 }
991
992 pub fn import_config(&mut self, json_data: &str) -> Result<(), String> {
994 let config: SyncConfig = serde_json::from_str(json_data)
995 .map_err(|e| format!("Failed to deserialize sync config: {}", e))?;
996
997 self.config = config;
998 Ok(())
999 }
1000
1001 pub fn has_pending_changes(&self) -> bool {
1003 self.status.pending_changes > 0
1004 }
1005
1006 pub fn get_manual_conflicts(&self) -> Vec<&SyncConflict> {
1008 self.conflicts
1009 .iter()
1010 .filter(|_conflict| {
1011 true
1013 })
1014 .collect()
1015 }
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1020pub struct SyncStats {
1021 pub total_syncs: usize,
1023 pub successful_syncs: usize,
1025 pub failed_syncs: usize,
1027 pub total_conflicts: usize,
1029 pub resolved_conflicts: usize,
1031 pub last_sync_duration_ms: Option<u64>,
1033}
1034
1035impl Default for SyncConfig {
1036 fn default() -> Self {
1037 Self {
1038 enabled: false,
1039 provider: SyncProvider::Local {
1040 directory_path: "./workspaces".to_string(),
1041 watch_changes: true,
1042 },
1043 interval_seconds: 300,
1044 conflict_strategy: ConflictResolutionStrategy::LocalWins,
1045 auto_commit: true,
1046 auto_push: false,
1047 directory_structure: SyncDirectoryStructure::PerWorkspace,
1048 sync_direction: SyncDirection::Bidirectional,
1049 }
1050 }
1051}
1052
1053impl Default for WorkspaceSyncManager {
1054 fn default() -> Self {
1055 Self::new(SyncConfig::default())
1056 }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062 use serde_json::json;
1063
1064 #[test]
1065 fn test_sync_config_creation() {
1066 let config = SyncConfig {
1067 enabled: true,
1068 provider: SyncProvider::Local {
1069 directory_path: "/tmp/sync".to_string(),
1070 watch_changes: true,
1071 },
1072 interval_seconds: 60,
1073 conflict_strategy: ConflictResolutionStrategy::LocalWins,
1074 auto_commit: true,
1075 auto_push: false,
1076 directory_structure: SyncDirectoryStructure::PerWorkspace,
1077 sync_direction: SyncDirection::Bidirectional,
1078 };
1079
1080 assert!(config.enabled);
1081 assert_eq!(config.interval_seconds, 60);
1082 assert!(config.auto_commit);
1083 assert!(!config.auto_push);
1084 }
1085
1086 #[test]
1087 fn test_sync_provider_git() {
1088 let provider = SyncProvider::Git {
1089 repo_url: "https://github.com/user/repo.git".to_string(),
1090 branch: "main".to_string(),
1091 auth_token: Some("token123".to_string()),
1092 };
1093
1094 match provider {
1095 SyncProvider::Git {
1096 repo_url,
1097 branch,
1098 auth_token,
1099 } => {
1100 assert_eq!(repo_url, "https://github.com/user/repo.git");
1101 assert_eq!(branch, "main");
1102 assert_eq!(auth_token, Some("token123".to_string()));
1103 }
1104 _ => panic!("Expected Git provider"),
1105 }
1106 }
1107
1108 #[test]
1109 fn test_sync_provider_cloud() {
1110 let provider = SyncProvider::Cloud {
1111 service_url: "https://api.example.com".to_string(),
1112 api_key: "key123".to_string(),
1113 project_id: "proj-456".to_string(),
1114 };
1115
1116 match provider {
1117 SyncProvider::Cloud {
1118 service_url,
1119 api_key,
1120 project_id,
1121 } => {
1122 assert_eq!(service_url, "https://api.example.com");
1123 assert_eq!(api_key, "key123");
1124 assert_eq!(project_id, "proj-456");
1125 }
1126 _ => panic!("Expected Cloud provider"),
1127 }
1128 }
1129
1130 #[test]
1131 fn test_sync_provider_local() {
1132 let provider = SyncProvider::Local {
1133 directory_path: "/tmp/sync".to_string(),
1134 watch_changes: true,
1135 };
1136
1137 match provider {
1138 SyncProvider::Local {
1139 directory_path,
1140 watch_changes,
1141 } => {
1142 assert_eq!(directory_path, "/tmp/sync");
1143 assert!(watch_changes);
1144 }
1145 _ => panic!("Expected Local provider"),
1146 }
1147 }
1148
1149 #[test]
1150 fn test_conflict_resolution_strategy_variants() {
1151 let local_wins = ConflictResolutionStrategy::LocalWins;
1152 let remote_wins = ConflictResolutionStrategy::RemoteWins;
1153 let manual = ConflictResolutionStrategy::Manual;
1154 let last_modified = ConflictResolutionStrategy::LastModified;
1155
1156 match local_wins {
1158 ConflictResolutionStrategy::LocalWins => {}
1159 _ => panic!(),
1160 }
1161 match remote_wins {
1162 ConflictResolutionStrategy::RemoteWins => {}
1163 _ => panic!(),
1164 }
1165 match manual {
1166 ConflictResolutionStrategy::Manual => {}
1167 _ => panic!(),
1168 }
1169 match last_modified {
1170 ConflictResolutionStrategy::LastModified => {}
1171 _ => panic!(),
1172 }
1173 }
1174
1175 #[test]
1176 fn test_sync_directory_structure_variants() {
1177 let single = SyncDirectoryStructure::SingleDirectory;
1178 let per_workspace = SyncDirectoryStructure::PerWorkspace;
1179 let hierarchical = SyncDirectoryStructure::Hierarchical;
1180
1181 match single {
1182 SyncDirectoryStructure::SingleDirectory => {}
1183 _ => panic!(),
1184 }
1185 match per_workspace {
1186 SyncDirectoryStructure::PerWorkspace => {}
1187 _ => panic!(),
1188 }
1189 match hierarchical {
1190 SyncDirectoryStructure::Hierarchical => {}
1191 _ => panic!(),
1192 }
1193 }
1194
1195 #[test]
1196 fn test_sync_direction_variants() {
1197 let bidirectional = SyncDirection::Bidirectional;
1198 let local_to_remote = SyncDirection::LocalToRemote;
1199 let remote_to_local = SyncDirection::RemoteToLocal;
1200
1201 match bidirectional {
1202 SyncDirection::Bidirectional => {}
1203 _ => panic!(),
1204 }
1205 match local_to_remote {
1206 SyncDirection::LocalToRemote => {}
1207 _ => panic!(),
1208 }
1209 match remote_to_local {
1210 SyncDirection::RemoteToLocal => {}
1211 _ => panic!(),
1212 }
1213 }
1214
1215 #[test]
1216 fn test_sync_state_variants() {
1217 let not_synced = SyncState::NotSynced;
1218 let syncing = SyncState::Syncing;
1219 let synced = SyncState::Synced;
1220 let sync_failed = SyncState::SyncFailed;
1221 let has_conflicts = SyncState::HasConflicts;
1222
1223 match not_synced {
1224 SyncState::NotSynced => {}
1225 _ => panic!(),
1226 }
1227 match syncing {
1228 SyncState::Syncing => {}
1229 _ => panic!(),
1230 }
1231 match synced {
1232 SyncState::Synced => {}
1233 _ => panic!(),
1234 }
1235 match sync_failed {
1236 SyncState::SyncFailed => {}
1237 _ => panic!(),
1238 }
1239 match has_conflicts {
1240 SyncState::HasConflicts => {}
1241 _ => panic!(),
1242 }
1243 }
1244
1245 #[test]
1246 fn test_sync_status_creation() {
1247 let status = SyncStatus {
1248 last_sync: Some(Utc::now()),
1249 state: SyncState::Synced,
1250 pending_changes: 5,
1251 conflicts: 2,
1252 last_error: Some("Test error".to_string()),
1253 };
1254
1255 assert!(status.last_sync.is_some());
1256 match status.state {
1257 SyncState::Synced => {}
1258 _ => panic!(),
1259 }
1260 assert_eq!(status.pending_changes, 5);
1261 assert_eq!(status.conflicts, 2);
1262 assert_eq!(status.last_error, Some("Test error".to_string()));
1263 }
1264
1265 #[test]
1266 fn test_sync_result_creation() {
1267 let result = SyncResult {
1268 success: true,
1269 changes_count: 10,
1270 conflicts: vec![],
1271 error: None,
1272 };
1273
1274 assert!(result.success);
1275 assert_eq!(result.changes_count, 10);
1276 assert!(result.conflicts.is_empty());
1277 assert!(result.error.is_none());
1278 }
1279
1280 #[test]
1281 fn test_sync_result_with_conflicts() {
1282 let conflict = SyncConflict {
1283 entity_id: EntityId::new(),
1284 entity_type: "request".to_string(),
1285 local_version: json!({"id": "local"}),
1286 remote_version: json!({"id": "remote"}),
1287 resolution: ConflictResolution::Manual,
1288 };
1289
1290 let result = SyncResult {
1291 success: false,
1292 changes_count: 0,
1293 conflicts: vec![conflict],
1294 error: Some("Conflicts detected".to_string()),
1295 };
1296
1297 assert!(!result.success);
1298 assert_eq!(result.conflicts.len(), 1);
1299 assert!(result.error.is_some());
1300 }
1301
1302 #[test]
1303 fn test_sync_conflict_creation() {
1304 let conflict = SyncConflict {
1305 entity_id: EntityId::new(),
1306 entity_type: "workspace".to_string(),
1307 local_version: json!({"name": "local"}),
1308 remote_version: json!({"name": "remote"}),
1309 resolution: ConflictResolution::Local,
1310 };
1311
1312 assert_eq!(conflict.entity_type, "workspace");
1313 match conflict.resolution {
1314 ConflictResolution::Local => {}
1315 _ => panic!(),
1316 }
1317 }
1318
1319 #[test]
1320 fn test_conflict_resolution_variants() {
1321 let local = ConflictResolution::Local;
1322 let remote = ConflictResolution::Remote;
1323 let manual = ConflictResolution::Manual;
1324
1325 match local {
1326 ConflictResolution::Local => {}
1327 _ => panic!(),
1328 }
1329 match remote {
1330 ConflictResolution::Remote => {}
1331 _ => panic!(),
1332 }
1333 match manual {
1334 ConflictResolution::Manual => {}
1335 _ => panic!(),
1336 }
1337 }
1338
1339 #[test]
1340 fn test_workspace_sync_manager_new() {
1341 let config = SyncConfig {
1342 enabled: true,
1343 provider: SyncProvider::Local {
1344 directory_path: "/tmp".to_string(),
1345 watch_changes: false,
1346 },
1347 interval_seconds: 30,
1348 conflict_strategy: ConflictResolutionStrategy::RemoteWins,
1349 auto_commit: false,
1350 auto_push: false,
1351 directory_structure: SyncDirectoryStructure::SingleDirectory,
1352 sync_direction: SyncDirection::LocalToRemote,
1353 };
1354
1355 let manager = WorkspaceSyncManager::new(config);
1356 assert!(manager.is_enabled());
1357 assert_eq!(manager.total_syncs, 0);
1358 assert_eq!(manager.successful_syncs, 0);
1359 assert_eq!(manager.failed_syncs, 0);
1360 }
1361
1362 #[test]
1363 fn test_workspace_sync_manager_default() {
1364 let manager = WorkspaceSyncManager::default();
1365 assert!(!manager.is_enabled());
1367 }
1368
1369 #[test]
1370 fn test_workspace_sync_manager_get_config() {
1371 let config = SyncConfig {
1372 enabled: false,
1373 provider: SyncProvider::Local {
1374 directory_path: "/tmp".to_string(),
1375 watch_changes: false,
1376 },
1377 interval_seconds: 60,
1378 conflict_strategy: ConflictResolutionStrategy::Manual,
1379 auto_commit: true,
1380 auto_push: true,
1381 directory_structure: SyncDirectoryStructure::Hierarchical,
1382 sync_direction: SyncDirection::Bidirectional,
1383 };
1384
1385 let manager = WorkspaceSyncManager::new(config);
1386 let retrieved_config = manager.get_config();
1387 assert!(!retrieved_config.enabled);
1388 assert_eq!(retrieved_config.interval_seconds, 60);
1389 }
1390
1391 #[test]
1392 fn test_workspace_sync_manager_update_config() {
1393 let config1 = SyncConfig {
1394 enabled: false,
1395 provider: SyncProvider::Local {
1396 directory_path: "/tmp1".to_string(),
1397 watch_changes: false,
1398 },
1399 interval_seconds: 30,
1400 conflict_strategy: ConflictResolutionStrategy::LocalWins,
1401 auto_commit: false,
1402 auto_push: false,
1403 directory_structure: SyncDirectoryStructure::SingleDirectory,
1404 sync_direction: SyncDirection::LocalToRemote,
1405 };
1406
1407 let mut manager = WorkspaceSyncManager::new(config1);
1408 assert!(!manager.is_enabled());
1409
1410 let config2 = SyncConfig {
1411 enabled: true,
1412 provider: SyncProvider::Local {
1413 directory_path: "/tmp2".to_string(),
1414 watch_changes: true,
1415 },
1416 interval_seconds: 120,
1417 conflict_strategy: ConflictResolutionStrategy::RemoteWins,
1418 auto_commit: true,
1419 auto_push: true,
1420 directory_structure: SyncDirectoryStructure::PerWorkspace,
1421 sync_direction: SyncDirection::Bidirectional,
1422 };
1423
1424 manager.update_config(config2);
1425 assert!(manager.is_enabled());
1426 assert_eq!(manager.get_config().interval_seconds, 120);
1427 }
1428
1429 #[test]
1430 fn test_workspace_sync_manager_get_status() {
1431 let config = SyncConfig {
1432 enabled: true,
1433 provider: SyncProvider::Local {
1434 directory_path: "/tmp".to_string(),
1435 watch_changes: false,
1436 },
1437 interval_seconds: 60,
1438 conflict_strategy: ConflictResolutionStrategy::LocalWins,
1439 auto_commit: false,
1440 auto_push: false,
1441 directory_structure: SyncDirectoryStructure::SingleDirectory,
1442 sync_direction: SyncDirection::Bidirectional,
1443 };
1444
1445 let manager = WorkspaceSyncManager::new(config);
1446 let status = manager.get_status();
1447 assert_eq!(status.pending_changes, 0);
1448 assert_eq!(status.conflicts, 0);
1449 match status.state {
1450 SyncState::NotSynced => {}
1451 _ => panic!(),
1452 }
1453 }
1454
1455 #[test]
1456 fn test_workspace_sync_manager_get_conflicts() {
1457 let config = SyncConfig {
1458 enabled: true,
1459 provider: SyncProvider::Local {
1460 directory_path: "/tmp".to_string(),
1461 watch_changes: false,
1462 },
1463 interval_seconds: 60,
1464 conflict_strategy: ConflictResolutionStrategy::Manual,
1465 auto_commit: false,
1466 auto_push: false,
1467 directory_structure: SyncDirectoryStructure::SingleDirectory,
1468 sync_direction: SyncDirection::Bidirectional,
1469 };
1470
1471 let manager = WorkspaceSyncManager::new(config);
1472 let conflicts = manager.get_conflicts();
1473 assert!(conflicts.is_empty());
1474 }
1475
1476 #[test]
1477 fn test_workspace_sync_manager_is_enabled() {
1478 let config_enabled = SyncConfig {
1479 enabled: true,
1480 provider: SyncProvider::Local {
1481 directory_path: "/tmp".to_string(),
1482 watch_changes: false,
1483 },
1484 interval_seconds: 60,
1485 conflict_strategy: ConflictResolutionStrategy::LocalWins,
1486 auto_commit: false,
1487 auto_push: false,
1488 directory_structure: SyncDirectoryStructure::SingleDirectory,
1489 sync_direction: SyncDirection::Bidirectional,
1490 };
1491
1492 let manager_enabled = WorkspaceSyncManager::new(config_enabled);
1493 assert!(manager_enabled.is_enabled());
1494
1495 let config_disabled = SyncConfig {
1496 enabled: false,
1497 provider: SyncProvider::Local {
1498 directory_path: "/tmp".to_string(),
1499 watch_changes: false,
1500 },
1501 interval_seconds: 60,
1502 conflict_strategy: ConflictResolutionStrategy::LocalWins,
1503 auto_commit: false,
1504 auto_push: false,
1505 directory_structure: SyncDirectoryStructure::SingleDirectory,
1506 sync_direction: SyncDirection::Bidirectional,
1507 };
1508
1509 let manager_disabled = WorkspaceSyncManager::new(config_disabled);
1510 assert!(!manager_disabled.is_enabled());
1511 }
1512
1513 #[tokio::test]
1514 async fn test_sync_workspace_disabled() {
1515 let config = SyncConfig {
1517 enabled: false,
1518 provider: SyncProvider::Local {
1519 directory_path: "/tmp/test".to_string(),
1520 watch_changes: false,
1521 },
1522 interval_seconds: 60,
1523 conflict_strategy: ConflictResolutionStrategy::Manual,
1524 auto_commit: false,
1525 auto_push: false,
1526 directory_structure: SyncDirectoryStructure::PerWorkspace,
1527 sync_direction: SyncDirection::Bidirectional,
1528 };
1529 let mut manager = WorkspaceSyncManager::new(config);
1530 let mut workspace = Workspace::new("Test Workspace".to_string());
1531
1532 let result = manager.sync_workspace(&mut workspace).await;
1534 assert!(result.is_err());
1535 assert!(result.unwrap_err().contains("disabled"));
1536 }
1537
1538 #[tokio::test]
1539 async fn test_sync_workspace_local_to_remote() {
1540 let temp_dir = tempfile::tempdir().unwrap();
1542 let config = SyncConfig {
1543 enabled: true,
1544 provider: SyncProvider::Local {
1545 directory_path: temp_dir.path().to_string_lossy().to_string(),
1546 watch_changes: false,
1547 },
1548 interval_seconds: 60,
1549 conflict_strategy: ConflictResolutionStrategy::Manual,
1550 auto_commit: false,
1551 auto_push: false,
1552 directory_structure: SyncDirectoryStructure::PerWorkspace,
1553 sync_direction: SyncDirection::LocalToRemote,
1554 };
1555 let mut manager = WorkspaceSyncManager::new(config);
1556 let mut workspace = Workspace::new("Test Workspace".to_string());
1557
1558 let result = manager.sync_workspace(&mut workspace).await;
1560 assert!(result.is_ok());
1561 let sync_result = result.unwrap();
1562 assert!(sync_result.success);
1563 assert_eq!(sync_result.changes_count, 1);
1564 assert!(matches!(manager.status.state, SyncState::Synced));
1565 assert_eq!(manager.total_syncs, 1);
1566 assert_eq!(manager.successful_syncs, 1);
1567 }
1568
1569 #[tokio::test]
1570 async fn test_sync_workspace_remote_to_local() {
1571 let temp_dir = tempfile::tempdir().unwrap();
1573 let config = SyncConfig {
1574 enabled: true,
1575 provider: SyncProvider::Local {
1576 directory_path: temp_dir.path().to_string_lossy().to_string(),
1577 watch_changes: false,
1578 },
1579 interval_seconds: 60,
1580 conflict_strategy: ConflictResolutionStrategy::Manual,
1581 auto_commit: false,
1582 auto_push: false,
1583 directory_structure: SyncDirectoryStructure::PerWorkspace,
1584 sync_direction: SyncDirection::RemoteToLocal,
1585 };
1586 let mut manager = WorkspaceSyncManager::new(config);
1587 let mut workspace = Workspace::new("Test Workspace".to_string());
1588
1589 let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
1591 let remote_workspace = Workspace::new("Remote Workspace".to_string());
1592 let content = serde_yaml::to_string(&remote_workspace).unwrap();
1593 tokio::fs::write(&file_path, content).await.unwrap();
1594
1595 let result = manager.sync_workspace(&mut workspace).await;
1597 assert!(result.is_ok());
1598 let sync_result = result.unwrap();
1599 assert!(sync_result.success);
1600 assert_eq!(workspace.name, "Remote Workspace");
1601 }
1602
1603 #[tokio::test]
1604 async fn test_sync_workspace_remote_to_local_file_not_found() {
1605 let temp_dir = tempfile::tempdir().unwrap();
1607 let config = SyncConfig {
1608 enabled: true,
1609 provider: SyncProvider::Local {
1610 directory_path: temp_dir.path().to_string_lossy().to_string(),
1611 watch_changes: false,
1612 },
1613 interval_seconds: 60,
1614 conflict_strategy: ConflictResolutionStrategy::Manual,
1615 auto_commit: false,
1616 auto_push: false,
1617 directory_structure: SyncDirectoryStructure::PerWorkspace,
1618 sync_direction: SyncDirection::RemoteToLocal,
1619 };
1620 let mut manager = WorkspaceSyncManager::new(config);
1621 let mut workspace = Workspace::new("Test Workspace".to_string());
1622
1623 let result = manager.sync_workspace(&mut workspace).await;
1625 assert!(result.is_err());
1626 assert!(result.unwrap_err().contains("not found"));
1627 }
1628
1629 #[tokio::test]
1630 async fn test_sync_workspace_bidirectional() {
1631 let temp_dir = tempfile::tempdir().unwrap();
1633 let config = SyncConfig {
1634 enabled: true,
1635 provider: SyncProvider::Local {
1636 directory_path: temp_dir.path().to_string_lossy().to_string(),
1637 watch_changes: false,
1638 },
1639 interval_seconds: 60,
1640 conflict_strategy: ConflictResolutionStrategy::Manual,
1641 auto_commit: false,
1642 auto_push: false,
1643 directory_structure: SyncDirectoryStructure::PerWorkspace,
1644 sync_direction: SyncDirection::Bidirectional,
1645 };
1646 let mut manager = WorkspaceSyncManager::new(config);
1647 let mut workspace = Workspace::new("Test Workspace".to_string());
1648
1649 let result = manager.sync_workspace(&mut workspace).await;
1651 assert!(result.is_ok());
1652 let sync_result = result.unwrap();
1653 assert!(sync_result.success);
1654 assert_eq!(sync_result.changes_count, 1);
1655 }
1656
1657 #[tokio::test]
1658 async fn test_sync_workspace_bidirectional_with_conflicts() {
1659 let temp_dir = tempfile::tempdir().unwrap();
1661 let config = SyncConfig {
1662 enabled: true,
1663 provider: SyncProvider::Local {
1664 directory_path: temp_dir.path().to_string_lossy().to_string(),
1665 watch_changes: false,
1666 },
1667 interval_seconds: 60,
1668 conflict_strategy: ConflictResolutionStrategy::Manual,
1669 auto_commit: false,
1670 auto_push: false,
1671 directory_structure: SyncDirectoryStructure::PerWorkspace,
1672 sync_direction: SyncDirection::Bidirectional,
1673 };
1674 let mut manager = WorkspaceSyncManager::new(config);
1675 let mut workspace = Workspace::new("Test Workspace".to_string());
1676
1677 workspace.updated_at = chrono::Utc::now();
1680 std::thread::sleep(std::time::Duration::from_millis(10));
1681 let mut remote_workspace = Workspace::new("Remote Workspace".to_string());
1683 remote_workspace.updated_at = chrono::Utc::now(); let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
1686 let content = serde_yaml::to_string(&remote_workspace).unwrap();
1687 tokio::fs::write(&file_path, content).await.unwrap();
1688
1689 let result = manager.sync_workspace(&mut workspace).await;
1691 assert!(result.is_ok());
1692 let sync_result = result.unwrap();
1693 assert!(sync_result.success);
1694 assert!(!sync_result.conflicts.is_empty());
1695 }
1696
1697 #[tokio::test]
1698 async fn test_sync_workspace_remote_to_local_with_conflicts() {
1699 let temp_dir = tempfile::tempdir().unwrap();
1701 let config = SyncConfig {
1702 enabled: true,
1703 provider: SyncProvider::Local {
1704 directory_path: temp_dir.path().to_string_lossy().to_string(),
1705 watch_changes: false,
1706 },
1707 interval_seconds: 60,
1708 conflict_strategy: ConflictResolutionStrategy::Manual,
1709 auto_commit: false,
1710 auto_push: false,
1711 directory_structure: SyncDirectoryStructure::PerWorkspace,
1712 sync_direction: SyncDirection::RemoteToLocal,
1713 };
1714 let mut manager = WorkspaceSyncManager::new(config);
1715 let mut workspace = Workspace::new("Test Workspace".to_string());
1716 let mut remote_workspace = Workspace::new("Remote Workspace".to_string());
1718 remote_workspace.updated_at = chrono::Utc::now();
1719 std::thread::sleep(std::time::Duration::from_millis(10));
1720 workspace.updated_at = chrono::Utc::now(); let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
1723 let content = serde_yaml::to_string(&remote_workspace).unwrap();
1724 tokio::fs::write(&file_path, content).await.unwrap();
1725
1726 let result = manager.sync_workspace(&mut workspace).await;
1728 assert!(result.is_ok());
1729 let sync_result = result.unwrap();
1730 assert!(sync_result.success);
1731 assert!(!sync_result.conflicts.is_empty());
1732 }
1733
1734 #[tokio::test]
1735 async fn test_sync_workspace_success_tracking() {
1736 let temp_dir = tempfile::tempdir().unwrap();
1738 let config = SyncConfig {
1739 enabled: true,
1740 provider: SyncProvider::Local {
1741 directory_path: temp_dir.path().to_string_lossy().to_string(),
1742 watch_changes: false,
1743 },
1744 interval_seconds: 60,
1745 conflict_strategy: ConflictResolutionStrategy::Manual,
1746 auto_commit: false,
1747 auto_push: false,
1748 directory_structure: SyncDirectoryStructure::PerWorkspace,
1749 sync_direction: SyncDirection::LocalToRemote,
1750 };
1751 let mut manager = WorkspaceSyncManager::new(config);
1752 let mut workspace = Workspace::new("Test Workspace".to_string());
1753
1754 let result = manager.sync_workspace(&mut workspace).await;
1756 assert!(result.is_ok());
1757 assert!(matches!(manager.status.state, SyncState::Synced));
1758 assert_eq!(manager.successful_syncs, 1);
1759 assert_eq!(manager.total_syncs, 1);
1760 assert!(manager.status.last_sync.is_some());
1761 assert_eq!(manager.status.pending_changes, 0);
1762 }
1763
1764 #[tokio::test]
1765 async fn test_sync_workspace_error_tracking() {
1766 let config = SyncConfig {
1768 enabled: true,
1769 provider: SyncProvider::Local {
1770 directory_path: "/nonexistent/path/that/does/not/exist".to_string(),
1771 watch_changes: false,
1772 },
1773 interval_seconds: 60,
1774 conflict_strategy: ConflictResolutionStrategy::Manual,
1775 auto_commit: false,
1776 auto_push: false,
1777 directory_structure: SyncDirectoryStructure::PerWorkspace,
1778 sync_direction: SyncDirection::RemoteToLocal,
1779 };
1780 let mut manager = WorkspaceSyncManager::new(config);
1781 let mut workspace = Workspace::new("Test Workspace".to_string());
1782
1783 let result = manager.sync_workspace(&mut workspace).await;
1785 if result.is_err() {
1787 assert!(matches!(manager.status.state, SyncState::SyncFailed));
1788 assert_eq!(manager.failed_syncs, 1);
1789 assert!(manager.status.last_error.is_some());
1790 }
1791 }
1792
1793 #[tokio::test]
1794 async fn test_sync_workspace_duration_tracking() {
1795 let temp_dir = tempfile::tempdir().unwrap();
1797 let config = SyncConfig {
1798 enabled: true,
1799 provider: SyncProvider::Local {
1800 directory_path: temp_dir.path().to_string_lossy().to_string(),
1801 watch_changes: false,
1802 },
1803 interval_seconds: 60,
1804 conflict_strategy: ConflictResolutionStrategy::Manual,
1805 auto_commit: false,
1806 auto_push: false,
1807 directory_structure: SyncDirectoryStructure::PerWorkspace,
1808 sync_direction: SyncDirection::LocalToRemote,
1809 };
1810 let mut manager = WorkspaceSyncManager::new(config);
1811 let mut workspace = Workspace::new("Test Workspace".to_string());
1812
1813 let result = manager.sync_workspace(&mut workspace).await;
1815 assert!(result.is_ok());
1816 assert!(manager.last_sync_duration_ms.is_some());
1817 assert!(manager.last_sync_duration_ms.unwrap() >= 0);
1818 }
1819
1820 #[tokio::test]
1821 async fn test_sync_workspace_state_transitions() {
1822 let temp_dir = tempfile::tempdir().unwrap();
1824 let config = SyncConfig {
1825 enabled: true,
1826 provider: SyncProvider::Local {
1827 directory_path: temp_dir.path().to_string_lossy().to_string(),
1828 watch_changes: false,
1829 },
1830 interval_seconds: 60,
1831 conflict_strategy: ConflictResolutionStrategy::Manual,
1832 auto_commit: false,
1833 auto_push: false,
1834 directory_structure: SyncDirectoryStructure::PerWorkspace,
1835 sync_direction: SyncDirection::LocalToRemote,
1836 };
1837 let mut manager = WorkspaceSyncManager::new(config);
1838 let mut workspace = Workspace::new("Test Workspace".to_string());
1839
1840 assert!(matches!(manager.status.state, SyncState::NotSynced));
1842 let result = manager.sync_workspace(&mut workspace).await;
1843 assert!(result.is_ok());
1844 assert!(matches!(manager.status.state, SyncState::Synced));
1845 }
1846}