use crate::workspace::{EntityId, Workspace};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json;
use serde_yaml;
use std::collections::HashMap;
use std::path::Path;
use tokio::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncConfig {
pub enabled: bool,
pub provider: SyncProvider,
pub interval_seconds: u64,
pub conflict_strategy: ConflictResolutionStrategy,
pub auto_commit: bool,
pub auto_push: bool,
pub directory_structure: SyncDirectoryStructure,
pub sync_direction: SyncDirection,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SyncProvider {
Git {
repo_url: String,
branch: String,
auth_token: Option<String>,
},
Cloud {
service_url: String,
api_key: String,
project_id: String,
},
Local {
directory_path: String,
watch_changes: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConflictResolutionStrategy {
LocalWins,
RemoteWins,
Manual,
LastModified,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SyncDirectoryStructure {
SingleDirectory,
PerWorkspace,
Hierarchical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SyncDirection {
Bidirectional,
LocalToRemote,
RemoteToLocal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStatus {
pub last_sync: Option<DateTime<Utc>>,
pub state: SyncState,
pub pending_changes: usize,
pub conflicts: usize,
pub last_error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SyncState {
NotSynced,
Syncing,
Synced,
SyncFailed,
HasConflicts,
}
#[derive(Debug, Clone)]
pub struct SyncResult {
pub success: bool,
pub changes_count: usize,
pub conflicts: Vec<SyncConflict>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncConflict {
pub entity_id: EntityId,
pub entity_type: String,
pub local_version: serde_json::Value,
pub remote_version: serde_json::Value,
pub resolution: ConflictResolution,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConflictResolution {
Local,
Remote,
Manual,
}
#[derive(Debug, Clone)]
pub struct WorkspaceSyncManager {
config: SyncConfig,
status: SyncStatus,
conflicts: Vec<SyncConflict>,
total_syncs: usize,
successful_syncs: usize,
failed_syncs: usize,
resolved_conflicts: usize,
last_sync_duration_ms: Option<u64>,
}
#[derive(Debug, Clone)]
pub enum SyncEvent {
Started,
Progress {
current: usize,
total: usize,
},
Completed(SyncResult),
Failed(String),
ConflictDetected(SyncConflict),
}
impl WorkspaceSyncManager {
pub fn new(config: SyncConfig) -> Self {
let status = SyncStatus {
last_sync: None,
state: SyncState::NotSynced,
pending_changes: 0,
conflicts: 0,
last_error: None,
};
Self {
config,
status,
conflicts: Vec::new(),
total_syncs: 0,
successful_syncs: 0,
failed_syncs: 0,
resolved_conflicts: 0,
last_sync_duration_ms: None,
}
}
pub fn get_config(&self) -> &SyncConfig {
&self.config
}
pub fn update_config(&mut self, config: SyncConfig) {
self.config = config;
}
pub fn get_status(&self) -> &SyncStatus {
&self.status
}
pub fn get_conflicts(&self) -> &[SyncConflict] {
&self.conflicts
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub async fn sync_workspace(
&mut self,
workspace: &mut Workspace,
) -> Result<SyncResult, String> {
if !self.config.enabled {
return Err("Synchronization is disabled".to_string());
}
self.total_syncs += 1;
let start_time = std::time::Instant::now();
self.status.state = SyncState::Syncing;
self.status.last_error = None;
let result = match &self.config.provider {
SyncProvider::Git {
repo_url,
branch,
auth_token,
} => self.sync_with_git(workspace, repo_url, branch, auth_token.as_deref()).await,
SyncProvider::Cloud {
service_url,
api_key,
project_id,
} => self.sync_with_cloud(workspace, service_url, api_key, project_id).await,
SyncProvider::Local {
directory_path,
watch_changes,
} => self.sync_with_local(workspace, directory_path, *watch_changes).await,
};
let duration = start_time.elapsed();
let duration_ms = duration.as_millis() as u64;
self.last_sync_duration_ms = Some(duration_ms);
match &result {
Ok(sync_result) => {
if sync_result.success {
self.successful_syncs += 1;
self.status.state = SyncState::Synced;
self.status.last_sync = Some(Utc::now());
self.status.pending_changes = 0;
self.status.conflicts = sync_result.conflicts.len();
} else {
self.failed_syncs += 1;
self.status.state = SyncState::SyncFailed;
self.status.last_error = sync_result.error.clone();
}
}
Err(error) => {
self.failed_syncs += 1;
self.status.state = SyncState::SyncFailed;
self.status.last_error = Some(error.clone());
}
}
result
}
async fn sync_with_git(
&self,
workspace: &mut Workspace,
repo_url: &str,
branch: &str,
auth_token: Option<&str>,
) -> Result<SyncResult, String> {
let temp_dir =
tempfile::tempdir().map_err(|e| format!("Failed to create temp directory: {}", e))?;
let repo_path = temp_dir.path().join("repo");
match self.config.sync_direction {
SyncDirection::LocalToRemote => {
self.sync_local_to_git(workspace, repo_url, branch, auth_token, &repo_path)
.await
}
SyncDirection::RemoteToLocal => {
self.sync_git_to_local(workspace, repo_url, branch, auth_token, &repo_path)
.await
}
SyncDirection::Bidirectional => {
self.sync_bidirectional_git(workspace, repo_url, branch, auth_token, &repo_path)
.await
}
}
}
async fn sync_local_to_git(
&self,
workspace: &Workspace,
repo_url: &str,
branch: &str,
auth_token: Option<&str>,
repo_path: &Path,
) -> Result<SyncResult, String> {
self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
let workspace_yaml = serde_yaml::to_string(workspace)
.map_err(|e| format!("Failed to serialize workspace: {}", e))?;
fs::write(&workspace_file, &workspace_yaml)
.await
.map_err(|e| format!("Failed to write workspace file: {}", e))?;
self.git_add_commit_push(repo_path, &workspace_file, auth_token).await?;
Ok(SyncResult {
success: true,
changes_count: 1,
conflicts: vec![],
error: None,
})
}
async fn sync_git_to_local(
&self,
workspace: &mut Workspace,
repo_url: &str,
branch: &str,
auth_token: Option<&str>,
repo_path: &Path,
) -> Result<SyncResult, String> {
self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
if !workspace_file.exists() {
return Ok(SyncResult {
success: true,
changes_count: 0,
conflicts: vec![],
error: None,
});
}
let workspace_yaml = fs::read_to_string(&workspace_file)
.await
.map_err(|e| format!("Failed to read workspace file: {}", e))?;
let remote_workspace: Workspace = serde_yaml::from_str(&workspace_yaml)
.map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
let conflicts = self.detect_conflicts(workspace, &remote_workspace);
if conflicts.is_empty() {
*workspace = remote_workspace;
Ok(SyncResult {
success: true,
changes_count: 1,
conflicts: vec![],
error: None,
})
} else {
Ok(SyncResult {
success: true,
changes_count: 0,
conflicts,
error: None,
})
}
}
async fn sync_bidirectional_git(
&self,
workspace: &mut Workspace,
repo_url: &str,
branch: &str,
auth_token: Option<&str>,
repo_path: &Path,
) -> Result<SyncResult, String> {
let pull_result = self
.sync_git_to_local(workspace, repo_url, branch, auth_token, repo_path)
.await?;
if !pull_result.conflicts.is_empty() {
return Ok(pull_result);
}
self.sync_local_to_git(workspace, repo_url, branch, auth_token, repo_path).await
}
async fn ensure_git_repo(
&self,
repo_url: &str,
branch: &str,
auth_token: Option<&str>,
repo_path: &Path,
) -> Result<(), String> {
use std::process::Command;
let repo_path_str = repo_path.to_string_lossy();
if repo_path.exists() {
let output = Command::new("git")
.args(["-C", repo_path_str.as_ref(), "pull", "origin", branch])
.output()
.map_err(|e| format!("Failed to pull repository: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Git pull failed: {}", stderr));
}
} else {
let clone_url = if let Some(token) = auth_token {
self.inject_auth_token_into_url(repo_url, token)
} else {
repo_url.to_string()
};
let output = Command::new("git")
.args([
"clone",
"--branch",
branch,
&clone_url,
repo_path_str.as_ref(),
])
.output()
.map_err(|e| format!("Failed to clone repository: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Git clone failed: {}", stderr));
}
}
Ok(())
}
async fn git_add_commit_push(
&self,
repo_path: &Path,
workspace_file: &Path,
_auth_token: Option<&str>,
) -> Result<(), String> {
use std::process::Command;
let repo_path_str = repo_path.to_string_lossy();
let file_path_str = workspace_file
.strip_prefix(repo_path)
.unwrap_or(workspace_file)
.to_string_lossy();
let output = Command::new("git")
.args(["-C", repo_path_str.as_ref(), "add", file_path_str.as_ref()])
.output()
.map_err(|e| format!("Failed to add file to git: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Git add failed: {}", stderr));
}
let status_output = Command::new("git")
.args(["-C", repo_path_str.as_ref(), "status", "--porcelain"])
.output()
.map_err(|e| format!("Failed to check git status: {}", e))?;
if status_output.stdout.is_empty() {
return Ok(());
}
let output = Command::new("git")
.args([
"-C",
repo_path_str.as_ref(),
"commit",
"-m",
"Update workspace",
])
.output()
.map_err(|e| format!("Failed to commit changes: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Git commit failed: {}", stderr));
}
let output = Command::new("git")
.args(["-C", repo_path_str.as_ref(), "push", "origin", "HEAD"])
.output()
.map_err(|e| format!("Failed to push changes: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Git push failed: {}", stderr));
}
Ok(())
}
fn inject_auth_token_into_url(&self, url: &str, token: &str) -> String {
if let Some(https_pos) = url.find("https://") {
let rest = &url[https_pos + "https://".len()..];
format!("https://oauth2:{}@{}", token, rest)
} else {
url.to_string()
}
}
async fn sync_with_cloud(
&self,
workspace: &mut Workspace,
service_url: &str,
api_key: &str,
project_id: &str,
) -> Result<SyncResult, String> {
let client = reqwest::Client::new();
let base_url = service_url.trim_end_matches('/');
let workspace_url =
format!("{}/api/v1/projects/{}/workspaces/{}", base_url, project_id, workspace.id);
match self.config.sync_direction {
SyncDirection::LocalToRemote => {
self.upload_workspace_to_cloud(&client, &workspace_url, api_key, workspace)
.await
}
SyncDirection::RemoteToLocal => {
self.download_workspace_from_cloud(&client, &workspace_url, api_key, workspace)
.await
}
SyncDirection::Bidirectional => {
self.bidirectional_sync(&client, &workspace_url, api_key, workspace).await
}
}
}
async fn upload_workspace_to_cloud(
&self,
client: &reqwest::Client,
workspace_url: &str,
api_key: &str,
workspace: &Workspace,
) -> Result<SyncResult, String> {
let workspace_json = serde_json::to_string(workspace)
.map_err(|e| format!("Failed to serialize workspace: {}", e))?;
let response = client
.put(workspace_url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.body(workspace_json)
.send()
.await
.map_err(|e| format!("Failed to upload workspace: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(format!("Cloud upload failed with status {}: {}", status, error_text));
}
Ok(SyncResult {
success: true,
changes_count: 1,
conflicts: vec![],
error: None,
})
}
async fn download_workspace_from_cloud(
&self,
client: &reqwest::Client,
workspace_url: &str,
api_key: &str,
workspace: &mut Workspace,
) -> Result<SyncResult, String> {
let response = client
.get(workspace_url)
.header("Authorization", format!("Bearer {}", api_key))
.send()
.await
.map_err(|e| format!("Failed to download workspace: {}", e))?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(SyncResult {
success: true,
changes_count: 0,
conflicts: vec![],
error: None,
});
}
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(format!("Cloud download failed with status {}: {}", status, error_text));
}
let remote_json: serde_json::Value = response
.json()
.await
.map_err(|e| format!("Failed to parse remote workspace: {}", e))?;
let remote_workspace: Workspace = serde_json::from_value(remote_json.clone())
.map_err(|e| format!("Failed to deserialize remote workspace: {}", e))?;
let conflicts = self.detect_conflicts(workspace, &remote_workspace);
if conflicts.is_empty() {
*workspace = remote_workspace;
Ok(SyncResult {
success: true,
changes_count: 1,
conflicts: vec![],
error: None,
})
} else {
Ok(SyncResult {
success: true,
changes_count: 0,
conflicts,
error: None,
})
}
}
async fn bidirectional_sync(
&self,
client: &reqwest::Client,
workspace_url: &str,
api_key: &str,
workspace: &mut Workspace,
) -> Result<SyncResult, String> {
let download_result = self
.download_workspace_from_cloud(client, workspace_url, api_key, workspace)
.await?;
if !download_result.conflicts.is_empty() {
return Ok(download_result);
}
self.upload_workspace_to_cloud(client, workspace_url, api_key, workspace).await
}
fn detect_conflicts(&self, local: &Workspace, remote: &Workspace) -> Vec<SyncConflict> {
let mut conflicts = vec![];
if local.updated_at > remote.updated_at {
let local_json = serde_json::to_value(local).unwrap_or_default();
let remote_json = serde_json::to_value(remote).unwrap_or_default();
if local_json != remote_json {
conflicts.push(SyncConflict {
entity_id: local.id.clone(),
entity_type: "workspace".to_string(),
local_version: local_json,
remote_version: remote_json,
resolution: ConflictResolution::Manual,
});
}
}
conflicts
}
async fn sync_with_local(
&self,
workspace: &mut Workspace,
directory_path: &str,
_watch_changes: bool,
) -> Result<SyncResult, String> {
let dir_path = Path::new(directory_path);
if !dir_path.exists() {
fs::create_dir_all(dir_path)
.await
.map_err(|e| format!("Failed to create directory {}: {}", directory_path, e))?;
}
match self.config.sync_direction {
SyncDirection::LocalToRemote => {
let file_path = dir_path.join(format!("{}.yaml", workspace.id));
let content = serde_yaml::to_string(workspace)
.map_err(|e| format!("Failed to serialize workspace: {}", e))?;
fs::write(&file_path, content)
.await
.map_err(|e| format!("Failed to write workspace file: {}", e))?;
Ok(SyncResult {
success: true,
changes_count: 1,
conflicts: vec![],
error: None,
})
}
SyncDirection::RemoteToLocal => {
let file_path = dir_path.join(format!("{}.yaml", workspace.id));
if !file_path.exists() {
return Err(format!("Workspace file not found: {:?}", file_path));
}
let content = fs::read_to_string(&file_path)
.await
.map_err(|e| format!("Failed to read workspace file: {}", e))?;
let remote_workspace: Workspace = serde_yaml::from_str(&content)
.map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
let conflicts = {
let mut conflicts = vec![];
if workspace.updated_at > remote_workspace.updated_at {
let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
let remote_json =
serde_json::to_value(&remote_workspace).unwrap_or_default();
conflicts.push(SyncConflict {
entity_id: workspace.id.clone(),
entity_type: "workspace".to_string(),
local_version: local_json,
remote_version: remote_json,
resolution: ConflictResolution::Manual,
});
} else if workspace.updated_at == remote_workspace.updated_at {
let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
let remote_json =
serde_json::to_value(&remote_workspace).unwrap_or_default();
if local_json != remote_json {
conflicts.push(SyncConflict {
entity_id: workspace.id.clone(),
entity_type: "workspace".to_string(),
local_version: local_json,
remote_version: remote_json,
resolution: ConflictResolution::Manual,
});
}
}
conflicts
};
if conflicts.is_empty() && remote_workspace.updated_at >= workspace.updated_at {
*workspace = remote_workspace;
Ok(SyncResult {
success: true,
changes_count: 1,
conflicts: vec![],
error: None,
})
} else {
Ok(SyncResult {
success: true,
changes_count: 0,
conflicts,
error: None,
})
}
}
SyncDirection::Bidirectional => {
let file_path = dir_path.join(format!("{}.yaml", workspace.id));
let mut conflicts = vec![];
if file_path.exists() {
let content = fs::read_to_string(&file_path)
.await
.map_err(|e| format!("Failed to read workspace file: {}", e))?;
let remote_workspace: Workspace = serde_yaml::from_str(&content)
.map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
if remote_workspace.updated_at > workspace.updated_at {
let remote_version =
serde_json::to_value(&remote_workspace).unwrap_or_default();
conflicts.push(SyncConflict {
entity_id: workspace.id.clone(),
entity_type: "workspace".to_string(),
local_version: serde_json::to_value(&*workspace).unwrap_or_default(),
remote_version,
resolution: ConflictResolution::Manual,
});
}
}
let content = serde_yaml::to_string(workspace)
.map_err(|e| format!("Failed to serialize workspace: {}", e))?;
fs::write(&file_path, content)
.await
.map_err(|e| format!("Failed to write workspace file: {}", e))?;
Ok(SyncResult {
success: true,
changes_count: 1,
conflicts,
error: None,
})
}
}
}
pub fn resolve_conflicts(
&mut self,
resolutions: HashMap<EntityId, ConflictResolution>,
) -> Result<usize, String> {
let mut resolved_count = 0;
for conflict in &self.conflicts.clone() {
if let Some(resolution) = resolutions.get(&conflict.entity_id) {
match resolution {
ConflictResolution::Local => {
resolved_count += 1;
}
ConflictResolution::Remote => {
resolved_count += 1;
}
ConflictResolution::Manual => {
continue;
}
}
}
}
self.resolved_conflicts += resolved_count;
self.conflicts.retain(|conflict| {
!resolutions.contains_key(&conflict.entity_id)
|| matches!(resolutions.get(&conflict.entity_id), Some(ConflictResolution::Manual))
});
self.status.conflicts = self.conflicts.len();
if self.conflicts.is_empty() {
self.status.state = SyncState::Synced;
} else {
self.status.state = SyncState::HasConflicts;
}
Ok(resolved_count)
}
pub fn get_sync_stats(&self) -> SyncStats {
SyncStats {
total_syncs: self.total_syncs,
successful_syncs: self.successful_syncs,
failed_syncs: self.failed_syncs,
total_conflicts: self.conflicts.len(),
resolved_conflicts: self.resolved_conflicts,
last_sync_duration_ms: self.last_sync_duration_ms,
}
}
pub fn export_config(&self) -> Result<String, String> {
serde_json::to_string_pretty(&self.config)
.map_err(|e| format!("Failed to serialize sync config: {}", e))
}
pub fn import_config(&mut self, json_data: &str) -> Result<(), String> {
let config: SyncConfig = serde_json::from_str(json_data)
.map_err(|e| format!("Failed to deserialize sync config: {}", e))?;
self.config = config;
Ok(())
}
pub fn has_pending_changes(&self) -> bool {
self.status.pending_changes > 0
}
pub fn get_manual_conflicts(&self) -> Vec<&SyncConflict> {
self.conflicts
.iter()
.filter(|_conflict| {
true
})
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStats {
pub total_syncs: usize,
pub successful_syncs: usize,
pub failed_syncs: usize,
pub total_conflicts: usize,
pub resolved_conflicts: usize,
pub last_sync_duration_ms: Option<u64>,
}
impl Default for SyncConfig {
fn default() -> Self {
Self {
enabled: false,
provider: SyncProvider::Local {
directory_path: "./workspaces".to_string(),
watch_changes: true,
},
interval_seconds: 300,
conflict_strategy: ConflictResolutionStrategy::LocalWins,
auto_commit: true,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::Bidirectional,
}
}
}
impl Default for WorkspaceSyncManager {
fn default() -> Self {
Self::new(SyncConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_sync_config_creation() {
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: "/tmp/sync".to_string(),
watch_changes: true,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::LocalWins,
auto_commit: true,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::Bidirectional,
};
assert!(config.enabled);
assert_eq!(config.interval_seconds, 60);
assert!(config.auto_commit);
assert!(!config.auto_push);
}
#[test]
fn test_sync_provider_git() {
let provider = SyncProvider::Git {
repo_url: "https://github.com/user/repo.git".to_string(),
branch: "main".to_string(),
auth_token: Some("token123".to_string()),
};
match provider {
SyncProvider::Git {
repo_url,
branch,
auth_token,
} => {
assert_eq!(repo_url, "https://github.com/user/repo.git");
assert_eq!(branch, "main");
assert_eq!(auth_token, Some("token123".to_string()));
}
_ => panic!("Expected Git provider"),
}
}
#[test]
fn test_sync_provider_cloud() {
let provider = SyncProvider::Cloud {
service_url: "https://api.example.com".to_string(),
api_key: "key123".to_string(),
project_id: "proj-456".to_string(),
};
match provider {
SyncProvider::Cloud {
service_url,
api_key,
project_id,
} => {
assert_eq!(service_url, "https://api.example.com");
assert_eq!(api_key, "key123");
assert_eq!(project_id, "proj-456");
}
_ => panic!("Expected Cloud provider"),
}
}
#[test]
fn test_sync_provider_local() {
let provider = SyncProvider::Local {
directory_path: "/tmp/sync".to_string(),
watch_changes: true,
};
match provider {
SyncProvider::Local {
directory_path,
watch_changes,
} => {
assert_eq!(directory_path, "/tmp/sync");
assert!(watch_changes);
}
_ => panic!("Expected Local provider"),
}
}
#[test]
fn test_conflict_resolution_strategy_variants() {
let local_wins = ConflictResolutionStrategy::LocalWins;
let remote_wins = ConflictResolutionStrategy::RemoteWins;
let manual = ConflictResolutionStrategy::Manual;
let last_modified = ConflictResolutionStrategy::LastModified;
match local_wins {
ConflictResolutionStrategy::LocalWins => {}
_ => panic!(),
}
match remote_wins {
ConflictResolutionStrategy::RemoteWins => {}
_ => panic!(),
}
match manual {
ConflictResolutionStrategy::Manual => {}
_ => panic!(),
}
match last_modified {
ConflictResolutionStrategy::LastModified => {}
_ => panic!(),
}
}
#[test]
fn test_sync_directory_structure_variants() {
let single = SyncDirectoryStructure::SingleDirectory;
let per_workspace = SyncDirectoryStructure::PerWorkspace;
let hierarchical = SyncDirectoryStructure::Hierarchical;
match single {
SyncDirectoryStructure::SingleDirectory => {}
_ => panic!(),
}
match per_workspace {
SyncDirectoryStructure::PerWorkspace => {}
_ => panic!(),
}
match hierarchical {
SyncDirectoryStructure::Hierarchical => {}
_ => panic!(),
}
}
#[test]
fn test_sync_direction_variants() {
let bidirectional = SyncDirection::Bidirectional;
let local_to_remote = SyncDirection::LocalToRemote;
let remote_to_local = SyncDirection::RemoteToLocal;
match bidirectional {
SyncDirection::Bidirectional => {}
_ => panic!(),
}
match local_to_remote {
SyncDirection::LocalToRemote => {}
_ => panic!(),
}
match remote_to_local {
SyncDirection::RemoteToLocal => {}
_ => panic!(),
}
}
#[test]
fn test_sync_state_variants() {
let not_synced = SyncState::NotSynced;
let syncing = SyncState::Syncing;
let synced = SyncState::Synced;
let sync_failed = SyncState::SyncFailed;
let has_conflicts = SyncState::HasConflicts;
match not_synced {
SyncState::NotSynced => {}
_ => panic!(),
}
match syncing {
SyncState::Syncing => {}
_ => panic!(),
}
match synced {
SyncState::Synced => {}
_ => panic!(),
}
match sync_failed {
SyncState::SyncFailed => {}
_ => panic!(),
}
match has_conflicts {
SyncState::HasConflicts => {}
_ => panic!(),
}
}
#[test]
fn test_sync_status_creation() {
let status = SyncStatus {
last_sync: Some(Utc::now()),
state: SyncState::Synced,
pending_changes: 5,
conflicts: 2,
last_error: Some("Test error".to_string()),
};
assert!(status.last_sync.is_some());
match status.state {
SyncState::Synced => {}
_ => panic!(),
}
assert_eq!(status.pending_changes, 5);
assert_eq!(status.conflicts, 2);
assert_eq!(status.last_error, Some("Test error".to_string()));
}
#[test]
fn test_sync_result_creation() {
let result = SyncResult {
success: true,
changes_count: 10,
conflicts: vec![],
error: None,
};
assert!(result.success);
assert_eq!(result.changes_count, 10);
assert!(result.conflicts.is_empty());
assert!(result.error.is_none());
}
#[test]
fn test_sync_result_with_conflicts() {
let conflict = SyncConflict {
entity_id: EntityId::new(),
entity_type: "request".to_string(),
local_version: json!({"id": "local"}),
remote_version: json!({"id": "remote"}),
resolution: ConflictResolution::Manual,
};
let result = SyncResult {
success: false,
changes_count: 0,
conflicts: vec![conflict],
error: Some("Conflicts detected".to_string()),
};
assert!(!result.success);
assert_eq!(result.conflicts.len(), 1);
assert!(result.error.is_some());
}
#[test]
fn test_sync_conflict_creation() {
let conflict = SyncConflict {
entity_id: EntityId::new(),
entity_type: "workspace".to_string(),
local_version: json!({"name": "local"}),
remote_version: json!({"name": "remote"}),
resolution: ConflictResolution::Local,
};
assert_eq!(conflict.entity_type, "workspace");
match conflict.resolution {
ConflictResolution::Local => {}
_ => panic!(),
}
}
#[test]
fn test_conflict_resolution_variants() {
let local = ConflictResolution::Local;
let remote = ConflictResolution::Remote;
let manual = ConflictResolution::Manual;
match local {
ConflictResolution::Local => {}
_ => panic!(),
}
match remote {
ConflictResolution::Remote => {}
_ => panic!(),
}
match manual {
ConflictResolution::Manual => {}
_ => panic!(),
}
}
#[test]
fn test_workspace_sync_manager_new() {
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: "/tmp".to_string(),
watch_changes: false,
},
interval_seconds: 30,
conflict_strategy: ConflictResolutionStrategy::RemoteWins,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::SingleDirectory,
sync_direction: SyncDirection::LocalToRemote,
};
let manager = WorkspaceSyncManager::new(config);
assert!(manager.is_enabled());
assert_eq!(manager.total_syncs, 0);
assert_eq!(manager.successful_syncs, 0);
assert_eq!(manager.failed_syncs, 0);
}
#[test]
fn test_workspace_sync_manager_default() {
let manager = WorkspaceSyncManager::default();
assert!(!manager.is_enabled());
}
#[test]
fn test_workspace_sync_manager_get_config() {
let config = SyncConfig {
enabled: false,
provider: SyncProvider::Local {
directory_path: "/tmp".to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: true,
auto_push: true,
directory_structure: SyncDirectoryStructure::Hierarchical,
sync_direction: SyncDirection::Bidirectional,
};
let manager = WorkspaceSyncManager::new(config);
let retrieved_config = manager.get_config();
assert!(!retrieved_config.enabled);
assert_eq!(retrieved_config.interval_seconds, 60);
}
#[test]
fn test_workspace_sync_manager_update_config() {
let config1 = SyncConfig {
enabled: false,
provider: SyncProvider::Local {
directory_path: "/tmp1".to_string(),
watch_changes: false,
},
interval_seconds: 30,
conflict_strategy: ConflictResolutionStrategy::LocalWins,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::SingleDirectory,
sync_direction: SyncDirection::LocalToRemote,
};
let mut manager = WorkspaceSyncManager::new(config1);
assert!(!manager.is_enabled());
let config2 = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: "/tmp2".to_string(),
watch_changes: true,
},
interval_seconds: 120,
conflict_strategy: ConflictResolutionStrategy::RemoteWins,
auto_commit: true,
auto_push: true,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::Bidirectional,
};
manager.update_config(config2);
assert!(manager.is_enabled());
assert_eq!(manager.get_config().interval_seconds, 120);
}
#[test]
fn test_workspace_sync_manager_get_status() {
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: "/tmp".to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::LocalWins,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::SingleDirectory,
sync_direction: SyncDirection::Bidirectional,
};
let manager = WorkspaceSyncManager::new(config);
let status = manager.get_status();
assert_eq!(status.pending_changes, 0);
assert_eq!(status.conflicts, 0);
match status.state {
SyncState::NotSynced => {}
_ => panic!(),
}
}
#[test]
fn test_workspace_sync_manager_get_conflicts() {
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: "/tmp".to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::SingleDirectory,
sync_direction: SyncDirection::Bidirectional,
};
let manager = WorkspaceSyncManager::new(config);
let conflicts = manager.get_conflicts();
assert!(conflicts.is_empty());
}
#[test]
fn test_workspace_sync_manager_is_enabled() {
let config_enabled = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: "/tmp".to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::LocalWins,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::SingleDirectory,
sync_direction: SyncDirection::Bidirectional,
};
let manager_enabled = WorkspaceSyncManager::new(config_enabled);
assert!(manager_enabled.is_enabled());
let config_disabled = SyncConfig {
enabled: false,
provider: SyncProvider::Local {
directory_path: "/tmp".to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::LocalWins,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::SingleDirectory,
sync_direction: SyncDirection::Bidirectional,
};
let manager_disabled = WorkspaceSyncManager::new(config_disabled);
assert!(!manager_disabled.is_enabled());
}
#[tokio::test]
async fn test_sync_workspace_disabled() {
let config = SyncConfig {
enabled: false,
provider: SyncProvider::Local {
directory_path: "/tmp/test".to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::Bidirectional,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("disabled"));
}
#[tokio::test]
async fn test_sync_workspace_local_to_remote() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::LocalToRemote,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_ok());
let sync_result = result.unwrap();
assert!(sync_result.success);
assert_eq!(sync_result.changes_count, 1);
assert!(matches!(manager.status.state, SyncState::Synced));
assert_eq!(manager.total_syncs, 1);
assert_eq!(manager.successful_syncs, 1);
}
#[tokio::test]
async fn test_sync_workspace_remote_to_local() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::RemoteToLocal,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
let remote_workspace = Workspace::new("Remote Workspace".to_string());
let content = serde_yaml::to_string(&remote_workspace).unwrap();
fs::write(&file_path, content).await.unwrap();
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_ok());
let sync_result = result.unwrap();
assert!(sync_result.success);
assert_eq!(workspace.name, "Remote Workspace");
}
#[tokio::test]
async fn test_sync_workspace_remote_to_local_file_not_found() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::RemoteToLocal,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("not found"));
}
#[tokio::test]
async fn test_sync_workspace_bidirectional() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::Bidirectional,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_ok());
let sync_result = result.unwrap();
assert!(sync_result.success);
assert_eq!(sync_result.changes_count, 1);
}
#[tokio::test]
async fn test_sync_workspace_bidirectional_with_conflicts() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::Bidirectional,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
workspace.updated_at = Utc::now();
std::thread::sleep(std::time::Duration::from_millis(10));
let mut remote_workspace = Workspace::new("Remote Workspace".to_string());
remote_workspace.updated_at = Utc::now();
let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
let content = serde_yaml::to_string(&remote_workspace).unwrap();
fs::write(&file_path, content).await.unwrap();
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_ok());
let sync_result = result.unwrap();
assert!(sync_result.success);
assert!(!sync_result.conflicts.is_empty());
}
#[tokio::test]
async fn test_sync_workspace_remote_to_local_with_conflicts() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::RemoteToLocal,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let mut remote_workspace = Workspace::new("Remote Workspace".to_string());
remote_workspace.updated_at = Utc::now();
std::thread::sleep(std::time::Duration::from_millis(10));
workspace.updated_at = Utc::now();
let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
let content = serde_yaml::to_string(&remote_workspace).unwrap();
fs::write(&file_path, content).await.unwrap();
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_ok());
let sync_result = result.unwrap();
assert!(sync_result.success);
assert!(!sync_result.conflicts.is_empty());
}
#[tokio::test]
async fn test_sync_workspace_success_tracking() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::LocalToRemote,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_ok());
assert!(matches!(manager.status.state, SyncState::Synced));
assert_eq!(manager.successful_syncs, 1);
assert_eq!(manager.total_syncs, 1);
assert!(manager.status.last_sync.is_some());
assert_eq!(manager.status.pending_changes, 0);
}
#[tokio::test]
async fn test_sync_workspace_error_tracking() {
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: "/nonexistent/path/that/does/not/exist".to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::RemoteToLocal,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let result = manager.sync_workspace(&mut workspace).await;
if result.is_err() {
assert!(matches!(manager.status.state, SyncState::SyncFailed));
assert_eq!(manager.failed_syncs, 1);
assert!(manager.status.last_error.is_some());
}
}
#[tokio::test]
async fn test_sync_workspace_duration_tracking() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::LocalToRemote,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_ok());
assert!(manager.last_sync_duration_ms.is_some());
let _ = manager.last_sync_duration_ms.unwrap(); }
#[tokio::test]
async fn test_sync_workspace_state_transitions() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SyncConfig {
enabled: true,
provider: SyncProvider::Local {
directory_path: temp_dir.path().to_string_lossy().to_string(),
watch_changes: false,
},
interval_seconds: 60,
conflict_strategy: ConflictResolutionStrategy::Manual,
auto_commit: false,
auto_push: false,
directory_structure: SyncDirectoryStructure::PerWorkspace,
sync_direction: SyncDirection::LocalToRemote,
};
let mut manager = WorkspaceSyncManager::new(config);
let mut workspace = Workspace::new("Test Workspace".to_string());
assert!(matches!(manager.status.state, SyncState::NotSynced));
let result = manager.sync_workspace(&mut workspace).await;
assert!(result.is_ok());
assert!(matches!(manager.status.state, SyncState::Synced));
}
}