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 #[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
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_real_handler_default() {
359 let handler = RealAppEffectHandler::default();
360 assert!(handler.workspace_root.is_none());
361 }
362
363 #[test]
364 fn test_real_handler_with_workspace_root() {
365 let root = PathBuf::from("/some/path");
366 let handler = RealAppEffectHandler::with_workspace_root(root.clone());
367 assert_eq!(handler.workspace_root, Some(root));
368 }
369
370 #[test]
371 fn test_resolve_path_absolute() {
372 let handler = RealAppEffectHandler::with_workspace_root(PathBuf::from("/workspace"));
373 let absolute = PathBuf::from("/absolute/path");
374 assert_eq!(handler.resolve_path(&absolute), absolute);
375 }
376
377 #[test]
378 fn test_resolve_path_relative_with_root() {
379 let handler = RealAppEffectHandler::with_workspace_root(PathBuf::from("/workspace"));
380 let relative = PathBuf::from("relative/path");
381 assert_eq!(
382 handler.resolve_path(&relative),
383 PathBuf::from("/workspace/relative/path")
384 );
385 }
386
387 #[test]
388 fn test_resolve_path_relative_without_root() {
389 let handler = RealAppEffectHandler::new();
390 let relative = PathBuf::from("relative/path");
391 assert_eq!(handler.resolve_path(&relative), relative);
392 }
393
394 #[test]
395 fn test_path_exists_effect() {
396 let mut handler = RealAppEffectHandler::new();
397 let result = handler.execute(AppEffect::PathExists {
399 path: PathBuf::from("."),
400 });
401 assert!(matches!(result, AppEffectResult::Bool(true)));
402 }
403
404 #[test]
405 fn test_path_not_exists_effect() {
406 let mut handler = RealAppEffectHandler::new();
407 let result = handler.execute(AppEffect::PathExists {
408 path: PathBuf::from("/nonexistent/path/that/should/not/exist/12345"),
409 });
410 assert!(matches!(result, AppEffectResult::Bool(false)));
411 }
412
413 #[test]
414 fn test_get_env_var_effect() {
415 let mut handler = RealAppEffectHandler::new();
416 let result = handler.execute(AppEffect::GetEnvVar {
418 name: "PATH".to_string(),
419 });
420 assert!(matches!(result, AppEffectResult::String(_)));
421 }
422
423 #[test]
424 fn test_get_env_var_not_set() {
425 let mut handler = RealAppEffectHandler::new();
426 let result = handler.execute(AppEffect::GetEnvVar {
427 name: "DEFINITELY_NOT_SET_ENV_VAR_12345".to_string(),
428 });
429 assert!(matches!(result, AppEffectResult::Error(_)));
430 }
431
432 #[test]
433 fn test_set_env_var_effect() {
434 let mut handler = RealAppEffectHandler::new();
435 let var_name = "TEST_RALPH_ENV_VAR_12345";
436
437 let result = handler.execute(AppEffect::SetEnvVar {
439 name: var_name.to_string(),
440 value: "test_value".to_string(),
441 });
442 assert!(matches!(result, AppEffectResult::Ok));
443
444 assert_eq!(std::env::var(var_name).ok(), Some("test_value".to_string()));
446
447 std::env::remove_var(var_name);
449 }
450
451 #[test]
452 fn test_logging_effects_are_noops() {
453 let mut handler = RealAppEffectHandler::new();
454
455 let effects = vec![
456 AppEffect::LogInfo {
457 message: "test".to_string(),
458 },
459 AppEffect::LogSuccess {
460 message: "test".to_string(),
461 },
462 AppEffect::LogWarn {
463 message: "test".to_string(),
464 },
465 AppEffect::LogError {
466 message: "test".to_string(),
467 },
468 ];
469
470 for effect in effects {
471 let result = handler.execute(effect);
472 assert!(
473 matches!(result, AppEffectResult::Ok),
474 "Logging effect should return Ok"
475 );
476 }
477 }
478
479 #[test]
480 fn test_rebase_effects_require_executor() {
481 let mut handler = RealAppEffectHandler::new();
482
483 let result = handler.execute(AppEffect::GitRebaseOnto {
484 upstream_branch: "main".to_string(),
485 });
486 assert!(matches!(result, AppEffectResult::Error(_)));
487
488 let result = handler.execute(AppEffect::GitContinueRebase);
489 assert!(matches!(result, AppEffectResult::Error(_)));
490
491 let result = handler.execute(AppEffect::GitAbortRebase);
492 assert!(matches!(result, AppEffectResult::Error(_)));
493 }
494}