1mod actions;
8mod executor;
9mod orchestrator;
10mod reporting;
11
12pub use actions::SyncAction;
14pub use orchestrator::{ApprovalCallback, SyncEngine};
15pub use reporting::SyncReporter;
16
17#[derive(Debug, Clone, Default)]
19pub struct SyncResult {
20 pub created: usize,
22 pub updated: usize,
24 pub deleted: usize,
26 pub skipped: usize,
28 pub skip_reasons: std::collections::HashMap<String, usize>,
30 pub conflicts: usize,
32 pub errors: Vec<String>,
34}
35
36impl SyncResult {
37 #[must_use]
39 pub const fn total_operations(&self) -> usize {
40 self.created + self.updated + self.deleted
41 }
42
43 #[must_use]
45 pub const fn is_success(&self) -> bool {
46 self.errors.is_empty()
47 }
48}
49
50#[cfg(test)]
51mod integration_tests {
52 use std::fs;
53 use std::path::Path;
54
55 use tempfile::TempDir;
56
57 use super::*;
58 use crate::comparison::ConflictStrategy;
59 use crate::config::{Config, SyncDirection};
60
61 fn setup_test_dirs() -> (TempDir, TempDir) {
62 let source = TempDir::new().unwrap();
63 let dest = TempDir::new().unwrap();
64 (source, dest)
65 }
66
67 fn create_test_file(dir: &Path, rel_path: &str, content: &str) {
68 let path = dir.join(rel_path);
69 if let Some(parent) = path.parent() {
70 fs::create_dir_all(parent).unwrap();
71 }
72 fs::write(path, content).unwrap();
73 }
74
75 #[test]
76 fn test_sync_create_new_files() {
77 let (source_dir, dest_dir) = setup_test_dirs();
78
79 create_test_file(source_dir.path(), "agents/test.md", "test agent");
81 create_test_file(source_dir.path(), "skills/skill1/SKILL.md", "test skill");
82
83 let config = Config::default();
84 let engine = SyncEngine::new(config, SyncDirection::ToLocal).unwrap();
85
86 let result = engine.sync(source_dir.path(), dest_dir.path()).unwrap();
87
88 assert_eq!(result.created, 2);
89 assert_eq!(result.updated, 0);
90 assert_eq!(result.skipped, 0);
91 assert!(result.is_success());
92
93 assert!(dest_dir.path().join("agents/test.md").exists());
95 assert!(dest_dir.path().join("skills/skill1/SKILL.md").exists());
96 }
97
98 #[test]
99 fn test_sync_skip_identical_files() {
100 let (source_dir, dest_dir) = setup_test_dirs();
101
102 let content = "identical content";
104 create_test_file(source_dir.path(), "agents/test.md", content);
105 create_test_file(dest_dir.path(), "agents/test.md", content);
106
107 let config = Config::default();
108 let engine = SyncEngine::new(config, SyncDirection::ToLocal).unwrap();
109
110 let result = engine.sync(source_dir.path(), dest_dir.path()).unwrap();
111
112 assert_eq!(result.created, 0);
113 assert_eq!(result.updated, 0);
114 assert_eq!(result.skipped, 1);
115 assert!(result.is_success());
116 }
117
118 #[test]
119 fn test_sync_with_ignore_patterns() {
120 let (source_dir, dest_dir) = setup_test_dirs();
121
122 create_test_file(source_dir.path(), "agents/include.md", "include");
123 create_test_file(source_dir.path(), "agents/ignore.md", "ignore");
124
125 let mut config = Config::default();
126 config.ignore = vec!["**/ignore.md".to_string()];
127
128 let engine = SyncEngine::new(config, SyncDirection::ToLocal).unwrap();
129 let result = engine.sync(source_dir.path(), dest_dir.path()).unwrap();
130
131 assert_eq!(result.created, 1);
132 assert_eq!(result.skipped, 1);
133 assert!(dest_dir.path().join("agents/include.md").exists());
134 assert!(!dest_dir.path().join("agents/ignore.md").exists());
135 }
136
137 #[test]
138 fn test_sync_conflict_fail_strategy() {
139 let (source_dir, dest_dir) = setup_test_dirs();
140
141 create_test_file(source_dir.path(), "agents/test.md", "source content");
143 create_test_file(dest_dir.path(), "agents/test.md", "dest content");
144
145 let config = Config::default(); let engine = SyncEngine::new(config, SyncDirection::ToLocal).unwrap();
147
148 let result = engine.sync(source_dir.path(), dest_dir.path());
150 assert!(result.is_err());
151
152 let err = result.unwrap_err();
153 let err_msg = err.to_string();
154 assert!(err_msg.contains("Sync failed"));
155 assert!(err_msg.contains("Conflict"));
156 }
157
158 #[test]
159 fn test_sync_dry_run() {
160 let (source_dir, dest_dir) = setup_test_dirs();
161
162 create_test_file(source_dir.path(), "agents/test.md", "test content");
163
164 let mut config = Config::default();
165 config.dry_run = Some(true);
166
167 let engine = SyncEngine::new(config, SyncDirection::ToLocal).unwrap();
168 let result = engine.sync(source_dir.path(), dest_dir.path()).unwrap();
169
170 assert_eq!(result.created, 1);
172 assert!(result.is_success());
173
174 assert!(!dest_dir.path().join("agents/test.md").exists());
176 }
177
178 #[test]
179 fn test_sync_bidirectional() {
180 let (dir1, dir2) = setup_test_dirs();
181
182 create_test_file(dir1.path(), "agents/from1.md", "content 1");
184
185 let config = Config::default();
186
187 let engine = SyncEngine::new(config.clone(), SyncDirection::ToLocal).unwrap();
189 let result = engine.sync(dir1.path(), dir2.path()).unwrap();
190 assert_eq!(result.created, 1);
191
192 create_test_file(dir2.path(), "agents/from2.md", "content 2");
194
195 let engine = SyncEngine::new(config, SyncDirection::ToGlobal).unwrap();
197 let result = engine.sync(dir2.path(), dir1.path()).unwrap();
198 assert_eq!(result.created, 1);
199
200 assert!(dir1.path().join("agents/from1.md").exists());
202 assert!(dir1.path().join("agents/from2.md").exists());
203 assert!(dir2.path().join("agents/from1.md").exists());
204 assert!(dir2.path().join("agents/from2.md").exists());
205 }
206
207 #[test]
208 fn test_sync_update_existing_files() {
209 let (source_dir, dest_dir) = setup_test_dirs();
210
211 create_test_file(source_dir.path(), "agents/test.md", "v1");
213 create_test_file(dest_dir.path(), "agents/test.md", "v1");
214
215 create_test_file(source_dir.path(), "agents/test.md", "v2");
217
218 let mut config = Config::default();
219 config.conflict_strategy = Some(ConflictStrategy::Overwrite);
220
221 let engine = SyncEngine::new(config, SyncDirection::ToLocal).unwrap();
222 let result = engine.sync(source_dir.path(), dest_dir.path()).unwrap();
223
224 assert_eq!(result.updated, 1);
226 assert_eq!(result.created, 0);
227 assert!(result.is_success());
228
229 let content = fs::read_to_string(dest_dir.path().join("agents/test.md")).unwrap();
231 assert_eq!(content, "v2");
232 }
233
234 #[test]
235 fn test_sync_reporter() {
236 let mut result = SyncResult::default();
237 result.created = 5;
238 result.updated = 3;
239 result.skipped = 2;
240
241 let summary = SyncReporter::generate_summary(&result);
242
243 assert!(summary.contains("Created: 5"));
244 assert!(summary.contains("Updated: 3"));
245 assert!(summary.contains("Skipped: 2"));
246 assert!(summary.contains("Total operations: 8"));
247 assert!(summary.contains("✓ Success"));
248 }
249
250 #[test]
251 fn test_sync_reporter_with_errors() {
252 let mut result = SyncResult::default();
253 result.created = 1;
254 result.errors.push("Test error".to_string());
255
256 let summary = SyncReporter::generate_summary(&result);
257
258 assert!(summary.contains("Errors (1)"));
259 assert!(summary.contains("Test error"));
260 assert!(summary.contains("✗ Completed with errors"));
261 assert!(!result.is_success());
262 }
263}