1use super::effect::{AppEffect, AppEffectHandler, AppEffectResult, CommitResult};
8use std::path::{Path, PathBuf};
9
10pub struct RealAppEffectHandler {
30 workspace_root: Option<PathBuf>,
35}
36
37impl RealAppEffectHandler {
38 pub fn new() -> Self {
42 Self {
43 workspace_root: None,
44 }
45 }
46
47 pub fn with_workspace_root(root: PathBuf) -> Self {
55 Self {
56 workspace_root: Some(root),
57 }
58 }
59
60 fn resolve_path(&self, path: &Path) -> PathBuf {
68 if path.is_absolute() {
69 path.to_path_buf()
70 } else if let Some(ref root) = self.workspace_root {
71 root.join(path)
72 } else {
73 path.to_path_buf()
74 }
75 }
76}
77
78impl Default for RealAppEffectHandler {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl AppEffectHandler for RealAppEffectHandler {
85 fn execute(&mut self, effect: AppEffect) -> AppEffectResult {
86 match effect {
87 AppEffect::SetCurrentDir { path } => {
91 let resolved = self.resolve_path(&path);
92 match std::env::set_current_dir(&resolved) {
93 Ok(()) => AppEffectResult::Ok,
94 Err(e) => AppEffectResult::Error(format!(
95 "Failed to set current directory to '{}': {}",
96 resolved.display(),
97 e
98 )),
99 }
100 }
101
102 AppEffect::WriteFile { path, content } => {
106 let resolved = self.resolve_path(&path);
107 if let Some(parent) = resolved.parent() {
109 if let Err(e) = std::fs::create_dir_all(parent) {
110 return AppEffectResult::Error(format!(
111 "Failed to create parent directories for '{}': {}",
112 resolved.display(),
113 e
114 ));
115 }
116 }
117 match std::fs::write(&resolved, content) {
118 Ok(()) => AppEffectResult::Ok,
119 Err(e) => AppEffectResult::Error(format!(
120 "Failed to write file '{}': {}",
121 resolved.display(),
122 e
123 )),
124 }
125 }
126
127 AppEffect::ReadFile { path } => {
128 let resolved = self.resolve_path(&path);
129 match std::fs::read_to_string(&resolved) {
130 Ok(content) => AppEffectResult::String(content),
131 Err(e) => AppEffectResult::Error(format!(
132 "Failed to read file '{}': {}",
133 resolved.display(),
134 e
135 )),
136 }
137 }
138
139 AppEffect::DeleteFile { path } => {
140 let resolved = self.resolve_path(&path);
141 match std::fs::remove_file(&resolved) {
142 Ok(()) => AppEffectResult::Ok,
143 Err(e) => AppEffectResult::Error(format!(
144 "Failed to delete file '{}': {}",
145 resolved.display(),
146 e
147 )),
148 }
149 }
150
151 AppEffect::CreateDir { path } => {
152 let resolved = self.resolve_path(&path);
153 match std::fs::create_dir_all(&resolved) {
154 Ok(()) => AppEffectResult::Ok,
155 Err(e) => AppEffectResult::Error(format!(
156 "Failed to create directory '{}': {}",
157 resolved.display(),
158 e
159 )),
160 }
161 }
162
163 AppEffect::PathExists { path } => {
164 let resolved = self.resolve_path(&path);
165 AppEffectResult::Bool(resolved.exists())
166 }
167
168 AppEffect::SetReadOnly { path, readonly } => {
169 let resolved = self.resolve_path(&path);
170 match std::fs::metadata(&resolved) {
171 Ok(metadata) => {
172 let mut permissions = metadata.permissions();
173 permissions.set_readonly(readonly);
174 match std::fs::set_permissions(&resolved, permissions) {
175 Ok(()) => AppEffectResult::Ok,
176 Err(e) => AppEffectResult::Error(format!(
177 "Failed to set permissions on '{}': {}",
178 resolved.display(),
179 e
180 )),
181 }
182 }
183 Err(e) => AppEffectResult::Error(format!(
184 "Failed to get metadata for '{}': {}",
185 resolved.display(),
186 e
187 )),
188 }
189 }
190
191 AppEffect::GitRequireRepo => match crate::git_helpers::require_git_repo() {
195 Ok(()) => AppEffectResult::Ok,
196 Err(e) => AppEffectResult::Error(format!("Not in a git repository: {}", e)),
197 },
198
199 AppEffect::GitGetRepoRoot => match crate::git_helpers::get_repo_root() {
200 Ok(root) => AppEffectResult::Path(root),
201 Err(e) => AppEffectResult::Error(format!("Failed to get repository root: {}", e)),
202 },
203
204 AppEffect::GitGetHeadOid => match crate::git_helpers::get_current_head_oid() {
205 Ok(oid) => AppEffectResult::String(oid),
206 Err(e) => AppEffectResult::Error(format!("Failed to get HEAD OID: {}", e)),
207 },
208
209 AppEffect::GitDiff => match crate::git_helpers::git_diff() {
210 Ok(diff) => AppEffectResult::String(diff),
211 Err(e) => AppEffectResult::Error(format!("Failed to get git diff: {}", e)),
212 },
213
214 AppEffect::GitDiffFrom { start_oid } => {
215 match crate::git_helpers::git_diff_from(&start_oid) {
216 Ok(diff) => AppEffectResult::String(diff),
217 Err(e) => AppEffectResult::Error(format!(
218 "Failed to get git diff from '{}': {}",
219 start_oid, e
220 )),
221 }
222 }
223
224 AppEffect::GitDiffFromStart => match crate::git_helpers::get_git_diff_from_start() {
225 Ok(diff) => AppEffectResult::String(diff),
226 Err(e) => {
227 AppEffectResult::Error(format!("Failed to get diff from start commit: {}", e))
228 }
229 },
230
231 AppEffect::GitSnapshot => match crate::git_helpers::git_snapshot() {
232 Ok(snapshot) => AppEffectResult::String(snapshot),
233 Err(e) => AppEffectResult::Error(format!("Failed to create git snapshot: {}", e)),
234 },
235
236 AppEffect::GitAddAll => match crate::git_helpers::git_add_all() {
237 Ok(staged) => AppEffectResult::Bool(staged),
238 Err(e) => AppEffectResult::Error(format!("Failed to stage all changes: {}", e)),
239 },
240
241 AppEffect::GitCommit {
242 message,
243 user_name,
244 user_email,
245 } => {
246 match crate::git_helpers::git_commit(
247 &message,
248 user_name.as_deref(),
249 user_email.as_deref(),
250 None, ) {
252 Ok(Some(oid)) => {
253 AppEffectResult::Commit(CommitResult::Success(oid.to_string()))
254 }
255 Ok(None) => AppEffectResult::Commit(CommitResult::NoChanges),
256 Err(e) => AppEffectResult::Error(format!("Failed to create commit: {}", e)),
257 }
258 }
259
260 AppEffect::GitSaveStartCommit => match crate::git_helpers::save_start_commit() {
261 Ok(()) => AppEffectResult::Ok,
262 Err(e) => AppEffectResult::Error(format!("Failed to save start commit: {}", e)),
263 },
264
265 AppEffect::GitResetStartCommit => match crate::git_helpers::reset_start_commit() {
266 Ok(result) => AppEffectResult::String(result.oid),
267 Err(e) => AppEffectResult::Error(format!("Failed to reset start commit: {}", e)),
268 },
269
270 AppEffect::GitRebaseOnto { upstream_branch: _ } => {
271 AppEffectResult::Error(
275 "GitRebaseOnto requires executor injection - use pipeline runner".to_string(),
276 )
277 }
278
279 AppEffect::GitGetConflictedFiles => match crate::git_helpers::get_conflicted_files() {
280 Ok(files) => AppEffectResult::StringList(files),
281 Err(e) => AppEffectResult::Error(format!("Failed to get conflicted files: {}", e)),
282 },
283
284 AppEffect::GitContinueRebase => {
285 AppEffectResult::Error(
287 "GitContinueRebase requires executor injection - use pipeline runner"
288 .to_string(),
289 )
290 }
291
292 AppEffect::GitAbortRebase => {
293 AppEffectResult::Error(
295 "GitAbortRebase requires executor injection - use pipeline runner".to_string(),
296 )
297 }
298
299 AppEffect::GitGetDefaultBranch => match crate::git_helpers::get_default_branch() {
300 Ok(branch) => AppEffectResult::String(branch),
301 Err(e) => AppEffectResult::Error(format!("Failed to get default branch: {}", e)),
302 },
303
304 AppEffect::GitIsMainBranch => match crate::git_helpers::is_main_or_master_branch() {
305 Ok(is_main) => AppEffectResult::Bool(is_main),
306 Err(e) => AppEffectResult::Error(format!("Failed to check branch: {}", e)),
307 },
308
309 AppEffect::GetEnvVar { name } => match std::env::var(&name) {
313 Ok(value) => AppEffectResult::String(value),
314 Err(std::env::VarError::NotPresent) => {
315 AppEffectResult::Error(format!("Environment variable '{}' not set", name))
316 }
317 Err(std::env::VarError::NotUnicode(_)) => AppEffectResult::Error(format!(
318 "Environment variable '{}' contains invalid Unicode",
319 name
320 )),
321 },
322
323 AppEffect::SetEnvVar { name, value } => {
324 std::env::set_var(&name, &value);
325 AppEffectResult::Ok
326 }
327
328 AppEffect::LogInfo { message: _ }
334 | AppEffect::LogSuccess { message: _ }
335 | AppEffect::LogWarn { message: _ }
336 | AppEffect::LogError { message: _ } => AppEffectResult::Ok,
337 }
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_real_handler_default() {
347 let handler = RealAppEffectHandler::default();
348 assert!(handler.workspace_root.is_none());
349 }
350
351 #[test]
352 fn test_real_handler_with_workspace_root() {
353 let root = PathBuf::from("/some/path");
354 let handler = RealAppEffectHandler::with_workspace_root(root.clone());
355 assert_eq!(handler.workspace_root, Some(root));
356 }
357
358 #[test]
359 fn test_resolve_path_absolute() {
360 let handler = RealAppEffectHandler::with_workspace_root(PathBuf::from("/workspace"));
361 let absolute = PathBuf::from("/absolute/path");
362 assert_eq!(handler.resolve_path(&absolute), absolute);
363 }
364
365 #[test]
366 fn test_resolve_path_relative_with_root() {
367 let handler = RealAppEffectHandler::with_workspace_root(PathBuf::from("/workspace"));
368 let relative = PathBuf::from("relative/path");
369 assert_eq!(
370 handler.resolve_path(&relative),
371 PathBuf::from("/workspace/relative/path")
372 );
373 }
374
375 #[test]
376 fn test_resolve_path_relative_without_root() {
377 let handler = RealAppEffectHandler::new();
378 let relative = PathBuf::from("relative/path");
379 assert_eq!(handler.resolve_path(&relative), relative);
380 }
381
382 #[test]
383 fn test_path_exists_effect() {
384 let mut handler = RealAppEffectHandler::new();
385 let result = handler.execute(AppEffect::PathExists {
387 path: PathBuf::from("."),
388 });
389 assert!(matches!(result, AppEffectResult::Bool(true)));
390 }
391
392 #[test]
393 fn test_path_not_exists_effect() {
394 let mut handler = RealAppEffectHandler::new();
395 let result = handler.execute(AppEffect::PathExists {
396 path: PathBuf::from("/nonexistent/path/that/should/not/exist/12345"),
397 });
398 assert!(matches!(result, AppEffectResult::Bool(false)));
399 }
400
401 #[test]
402 fn test_get_env_var_effect() {
403 let mut handler = RealAppEffectHandler::new();
404 let result = handler.execute(AppEffect::GetEnvVar {
406 name: "PATH".to_string(),
407 });
408 assert!(matches!(result, AppEffectResult::String(_)));
409 }
410
411 #[test]
412 fn test_get_env_var_not_set() {
413 let mut handler = RealAppEffectHandler::new();
414 let result = handler.execute(AppEffect::GetEnvVar {
415 name: "DEFINITELY_NOT_SET_ENV_VAR_12345".to_string(),
416 });
417 assert!(matches!(result, AppEffectResult::Error(_)));
418 }
419
420 #[test]
421 fn test_set_env_var_effect() {
422 let mut handler = RealAppEffectHandler::new();
423 let var_name = "TEST_RALPH_ENV_VAR_12345";
424
425 let result = handler.execute(AppEffect::SetEnvVar {
427 name: var_name.to_string(),
428 value: "test_value".to_string(),
429 });
430 assert!(matches!(result, AppEffectResult::Ok));
431
432 assert_eq!(std::env::var(var_name).ok(), Some("test_value".to_string()));
434
435 std::env::remove_var(var_name);
437 }
438
439 #[test]
440 fn test_logging_effects_are_noops() {
441 let mut handler = RealAppEffectHandler::new();
442
443 let effects = vec![
444 AppEffect::LogInfo {
445 message: "test".to_string(),
446 },
447 AppEffect::LogSuccess {
448 message: "test".to_string(),
449 },
450 AppEffect::LogWarn {
451 message: "test".to_string(),
452 },
453 AppEffect::LogError {
454 message: "test".to_string(),
455 },
456 ];
457
458 for effect in effects {
459 let result = handler.execute(effect);
460 assert!(
461 matches!(result, AppEffectResult::Ok),
462 "Logging effect should return Ok"
463 );
464 }
465 }
466
467 #[test]
468 fn test_rebase_effects_require_executor() {
469 let mut handler = RealAppEffectHandler::new();
470
471 let result = handler.execute(AppEffect::GitRebaseOnto {
472 upstream_branch: "main".to_string(),
473 });
474 assert!(matches!(result, AppEffectResult::Error(_)));
475
476 let result = handler.execute(AppEffect::GitContinueRebase);
477 assert!(matches!(result, AppEffectResult::Error(_)));
478
479 let result = handler.execute(AppEffect::GitAbortRebase);
480 assert!(matches!(result, AppEffectResult::Error(_)));
481 }
482}