1use super::effect::{AppEffect, AppEffectHandler, AppEffectResult, CommitResult, RebaseResult};
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 #[cfg(any(test, feature = "test-utils"))]
215 AppEffect::GitDiffFrom { start_oid } => {
216 match crate::git_helpers::git_diff_from(&start_oid) {
217 Ok(diff) => AppEffectResult::String(diff),
218 Err(e) => AppEffectResult::Error(format!(
219 "Failed to get git diff from '{}': {}",
220 start_oid, e
221 )),
222 }
223 }
224
225 #[cfg(not(any(test, feature = "test-utils")))]
226 AppEffect::GitDiffFrom { start_oid: _ } => {
227 AppEffectResult::Error("GitDiffFrom requires test-utils feature".to_string())
228 }
229
230 #[cfg(any(test, feature = "test-utils"))]
231 AppEffect::GitDiffFromStart => match crate::git_helpers::get_git_diff_from_start() {
232 Ok(diff) => AppEffectResult::String(diff),
233 Err(e) => {
234 AppEffectResult::Error(format!("Failed to get diff from start commit: {}", e))
235 }
236 },
237
238 #[cfg(not(any(test, feature = "test-utils")))]
239 AppEffect::GitDiffFromStart => {
240 AppEffectResult::Error("GitDiffFromStart requires test-utils feature".to_string())
241 }
242
243 AppEffect::GitSnapshot => match crate::git_helpers::git_snapshot() {
244 Ok(snapshot) => AppEffectResult::String(snapshot),
245 Err(e) => AppEffectResult::Error(format!("Failed to create git snapshot: {}", e)),
246 },
247
248 AppEffect::GitAddAll => match crate::git_helpers::git_add_all() {
249 Ok(staged) => AppEffectResult::Bool(staged),
250 Err(e) => AppEffectResult::Error(format!("Failed to stage all changes: {}", e)),
251 },
252
253 AppEffect::GitCommit {
254 message,
255 user_name,
256 user_email,
257 } => {
258 match crate::git_helpers::git_commit(
259 &message,
260 user_name.as_deref(),
261 user_email.as_deref(),
262 None, ) {
264 Ok(Some(oid)) => {
265 AppEffectResult::Commit(CommitResult::Success(oid.to_string()))
266 }
267 Ok(None) => AppEffectResult::Commit(CommitResult::NoChanges),
268 Err(e) => AppEffectResult::Error(format!("Failed to create commit: {}", e)),
269 }
270 }
271
272 AppEffect::GitSaveStartCommit => match crate::git_helpers::save_start_commit() {
273 Ok(()) => AppEffectResult::Ok,
274 Err(e) => AppEffectResult::Error(format!("Failed to save start commit: {}", e)),
275 },
276
277 AppEffect::GitResetStartCommit => match crate::git_helpers::reset_start_commit() {
278 Ok(result) => AppEffectResult::String(result.oid),
279 Err(e) => AppEffectResult::Error(format!("Failed to reset start commit: {}", e)),
280 },
281
282 AppEffect::GitRebaseOnto { upstream_branch: _ } => {
283 AppEffectResult::Error(
287 "GitRebaseOnto requires executor injection - use pipeline runner".to_string(),
288 )
289 }
290
291 AppEffect::GitGetConflictedFiles => match crate::git_helpers::get_conflicted_files() {
292 Ok(files) => AppEffectResult::StringList(files),
293 Err(e) => AppEffectResult::Error(format!("Failed to get conflicted files: {}", e)),
294 },
295
296 AppEffect::GitContinueRebase => {
297 AppEffectResult::Error(
299 "GitContinueRebase requires executor injection - use pipeline runner"
300 .to_string(),
301 )
302 }
303
304 AppEffect::GitAbortRebase => {
305 AppEffectResult::Error(
307 "GitAbortRebase requires executor injection - use pipeline runner".to_string(),
308 )
309 }
310
311 AppEffect::GitGetDefaultBranch => match crate::git_helpers::get_default_branch() {
312 Ok(branch) => AppEffectResult::String(branch),
313 Err(e) => AppEffectResult::Error(format!("Failed to get default branch: {}", e)),
314 },
315
316 AppEffect::GitIsMainBranch => match crate::git_helpers::is_main_or_master_branch() {
317 Ok(is_main) => AppEffectResult::Bool(is_main),
318 Err(e) => AppEffectResult::Error(format!("Failed to check branch: {}", e)),
319 },
320
321 AppEffect::GetEnvVar { name } => match std::env::var(&name) {
325 Ok(value) => AppEffectResult::String(value),
326 Err(std::env::VarError::NotPresent) => {
327 AppEffectResult::Error(format!("Environment variable '{}' not set", name))
328 }
329 Err(std::env::VarError::NotUnicode(_)) => AppEffectResult::Error(format!(
330 "Environment variable '{}' contains invalid Unicode",
331 name
332 )),
333 },
334
335 AppEffect::SetEnvVar { name, value } => {
336 std::env::set_var(&name, &value);
337 AppEffectResult::Ok
338 }
339
340 AppEffect::LogInfo { message: _ }
346 | AppEffect::LogSuccess { message: _ }
347 | AppEffect::LogWarn { message: _ }
348 | AppEffect::LogError { message: _ } => AppEffectResult::Ok,
349 }
350 }
351}
352
353pub struct RealAppEffectHandlerWithExecutor<'a> {
358 inner: RealAppEffectHandler,
360 executor: &'a dyn crate::executor::ProcessExecutor,
362}
363
364impl<'a> RealAppEffectHandlerWithExecutor<'a> {
365 pub fn new(executor: &'a dyn crate::executor::ProcessExecutor) -> Self {
371 Self {
372 inner: RealAppEffectHandler::new(),
373 executor,
374 }
375 }
376
377 pub fn with_workspace_root(
384 root: PathBuf,
385 executor: &'a dyn crate::executor::ProcessExecutor,
386 ) -> Self {
387 Self {
388 inner: RealAppEffectHandler::with_workspace_root(root),
389 executor,
390 }
391 }
392}
393
394impl AppEffectHandler for RealAppEffectHandlerWithExecutor<'_> {
395 fn execute(&mut self, effect: AppEffect) -> AppEffectResult {
396 match &effect {
397 AppEffect::GitRebaseOnto { upstream_branch } => {
398 match crate::git_helpers::rebase_onto(upstream_branch, self.executor) {
399 Ok(result) => match result {
400 crate::git_helpers::RebaseResult::Success => {
401 AppEffectResult::Rebase(RebaseResult::Success)
402 }
403 crate::git_helpers::RebaseResult::Conflicts(files) => {
404 AppEffectResult::Rebase(RebaseResult::Conflicts(files))
405 }
406 crate::git_helpers::RebaseResult::NoOp { reason } => {
407 AppEffectResult::Rebase(RebaseResult::NoOp { reason })
408 }
409 crate::git_helpers::RebaseResult::Failed(kind) => {
410 AppEffectResult::Rebase(RebaseResult::Failed(kind.to_string()))
411 }
412 },
413 Err(e) => AppEffectResult::Error(format!("Rebase failed: {}", e)),
414 }
415 }
416
417 AppEffect::GitContinueRebase => {
418 match crate::git_helpers::continue_rebase(self.executor) {
419 Ok(()) => AppEffectResult::Ok,
420 Err(e) => AppEffectResult::Error(format!("Failed to continue rebase: {}", e)),
421 }
422 }
423
424 AppEffect::GitAbortRebase => match crate::git_helpers::abort_rebase(self.executor) {
425 Ok(()) => AppEffectResult::Ok,
426 Err(e) => AppEffectResult::Error(format!("Failed to abort rebase: {}", e)),
427 },
428
429 _ => self.inner.execute(effect),
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn test_real_handler_default() {
441 let handler = RealAppEffectHandler::default();
442 assert!(handler.workspace_root.is_none());
443 }
444
445 #[test]
446 fn test_real_handler_with_workspace_root() {
447 let root = PathBuf::from("/some/path");
448 let handler = RealAppEffectHandler::with_workspace_root(root.clone());
449 assert_eq!(handler.workspace_root, Some(root));
450 }
451
452 #[test]
453 fn test_resolve_path_absolute() {
454 let handler = RealAppEffectHandler::with_workspace_root(PathBuf::from("/workspace"));
455 let absolute = PathBuf::from("/absolute/path");
456 assert_eq!(handler.resolve_path(&absolute), absolute);
457 }
458
459 #[test]
460 fn test_resolve_path_relative_with_root() {
461 let handler = RealAppEffectHandler::with_workspace_root(PathBuf::from("/workspace"));
462 let relative = PathBuf::from("relative/path");
463 assert_eq!(
464 handler.resolve_path(&relative),
465 PathBuf::from("/workspace/relative/path")
466 );
467 }
468
469 #[test]
470 fn test_resolve_path_relative_without_root() {
471 let handler = RealAppEffectHandler::new();
472 let relative = PathBuf::from("relative/path");
473 assert_eq!(handler.resolve_path(&relative), relative);
474 }
475
476 #[test]
477 fn test_path_exists_effect() {
478 let mut handler = RealAppEffectHandler::new();
479 let result = handler.execute(AppEffect::PathExists {
481 path: PathBuf::from("."),
482 });
483 assert!(matches!(result, AppEffectResult::Bool(true)));
484 }
485
486 #[test]
487 fn test_path_not_exists_effect() {
488 let mut handler = RealAppEffectHandler::new();
489 let result = handler.execute(AppEffect::PathExists {
490 path: PathBuf::from("/nonexistent/path/that/should/not/exist/12345"),
491 });
492 assert!(matches!(result, AppEffectResult::Bool(false)));
493 }
494
495 #[test]
496 fn test_get_env_var_effect() {
497 let mut handler = RealAppEffectHandler::new();
498 let result = handler.execute(AppEffect::GetEnvVar {
500 name: "PATH".to_string(),
501 });
502 assert!(matches!(result, AppEffectResult::String(_)));
503 }
504
505 #[test]
506 fn test_get_env_var_not_set() {
507 let mut handler = RealAppEffectHandler::new();
508 let result = handler.execute(AppEffect::GetEnvVar {
509 name: "DEFINITELY_NOT_SET_ENV_VAR_12345".to_string(),
510 });
511 assert!(matches!(result, AppEffectResult::Error(_)));
512 }
513
514 #[test]
515 fn test_set_env_var_effect() {
516 let mut handler = RealAppEffectHandler::new();
517 let var_name = "TEST_RALPH_ENV_VAR_12345";
518
519 let result = handler.execute(AppEffect::SetEnvVar {
521 name: var_name.to_string(),
522 value: "test_value".to_string(),
523 });
524 assert!(matches!(result, AppEffectResult::Ok));
525
526 assert_eq!(std::env::var(var_name).ok(), Some("test_value".to_string()));
528
529 std::env::remove_var(var_name);
531 }
532
533 #[test]
534 fn test_logging_effects_are_noops() {
535 let mut handler = RealAppEffectHandler::new();
536
537 let effects = vec![
538 AppEffect::LogInfo {
539 message: "test".to_string(),
540 },
541 AppEffect::LogSuccess {
542 message: "test".to_string(),
543 },
544 AppEffect::LogWarn {
545 message: "test".to_string(),
546 },
547 AppEffect::LogError {
548 message: "test".to_string(),
549 },
550 ];
551
552 for effect in effects {
553 let result = handler.execute(effect);
554 assert!(
555 matches!(result, AppEffectResult::Ok),
556 "Logging effect should return Ok"
557 );
558 }
559 }
560
561 #[test]
562 fn test_rebase_effects_require_executor() {
563 let mut handler = RealAppEffectHandler::new();
564
565 let result = handler.execute(AppEffect::GitRebaseOnto {
566 upstream_branch: "main".to_string(),
567 });
568 assert!(matches!(result, AppEffectResult::Error(_)));
569
570 let result = handler.execute(AppEffect::GitContinueRebase);
571 assert!(matches!(result, AppEffectResult::Error(_)));
572
573 let result = handler.execute(AppEffect::GitAbortRebase);
574 assert!(matches!(result, AppEffectResult::Error(_)));
575 }
576}