1use std::fs;
8use std::process::Command;
9
10use crate::config::spec_dir;
11use crate::error::Result;
12use crate::spec::{Spec, UserStory};
13use crate::state::StateManager;
14use crate::worktree::get_main_repo_root;
15
16pub const SELF_TEST_BRANCH: &str = "autom8/self-test";
18
19pub const SELF_TEST_FILE: &str = "test_output.txt";
21
22pub const SELF_TEST_SPEC_FILENAME: &str = "test_spec.json";
24
25pub fn create_self_test_spec() -> Spec {
35 Spec {
36 project: "autom8-self-test".to_string(),
37 branch_name: SELF_TEST_BRANCH.to_string(),
38 description: "Self-test spec for validating autom8 functionality. Creates and modifies a dummy test_output.txt file.".to_string(),
39 user_stories: vec![
40 UserStory {
41 id: "ST-001".to_string(),
42 title: "Create test output file".to_string(),
43 description: "Create the test_output.txt file in the repository root with an initial greeting message.".to_string(),
44 acceptance_criteria: vec![
45 "File test_output.txt exists in the repo root".to_string(),
46 "File contains the text 'Hello from autom8 self-test!'".to_string(),
47 ],
48 priority: 1,
49 passes: false,
50 notes: "This is the first step - just create the file with a simple greeting.".to_string(),
51 },
52 UserStory {
53 id: "ST-002".to_string(),
54 title: "Add separator and status line".to_string(),
55 description: "Add a separator line and a status line to test_output.txt.".to_string(),
56 acceptance_criteria: vec![
57 "File contains a separator line (e.g., '---')".to_string(),
58 "File contains a status line with 'Status: Running'".to_string(),
59 ],
60 priority: 2,
61 passes: false,
62 notes: "Appends content to the existing file.".to_string(),
63 },
64 UserStory {
65 id: "ST-003".to_string(),
66 title: "Add completion message".to_string(),
67 description: "Add a final completion message to test_output.txt indicating the self-test finished successfully.".to_string(),
68 acceptance_criteria: vec![
69 "File contains a completion message".to_string(),
70 "Message includes 'Self-test complete!'".to_string(),
71 ],
72 priority: 3,
73 passes: false,
74 notes: "Final step - adds the completion marker.".to_string(),
75 },
76 ],
77 }
78}
79
80#[derive(Debug, Default)]
82pub struct CleanupResult {
83 pub test_file_deleted: bool,
85 pub spec_file_deleted: bool,
87 pub session_cleared: bool,
89 pub branch_deleted: bool,
91 pub worktree_deleted: bool,
93 pub errors: Vec<String>,
95}
96
97impl CleanupResult {
98 pub fn is_complete(&self) -> bool {
100 self.errors.is_empty()
101 }
102}
103
104pub fn cleanup_self_test() -> CleanupResult {
116 let mut result = CleanupResult::default();
117
118 let worktree_info = get_worktree_info_for_cleanup();
120
121 result.test_file_deleted = cleanup_test_file(&mut result.errors);
123
124 result.spec_file_deleted = cleanup_spec_file(&mut result.errors);
126
127 result.session_cleared = cleanup_session_state(&mut result.errors);
129
130 if let Some((worktree_path, main_repo_path)) = worktree_info {
132 result.worktree_deleted =
133 cleanup_worktree(&worktree_path, &main_repo_path, &mut result.errors);
134 }
135
136 result.branch_deleted = cleanup_test_branch(&mut result.errors);
138
139 result
140}
141
142fn get_worktree_info_for_cleanup() -> Option<(std::path::PathBuf, std::path::PathBuf)> {
145 use crate::worktree::{get_main_repo_root, is_in_worktree};
146
147 if is_in_worktree().unwrap_or(false) {
149 let worktree_path = std::env::current_dir().ok()?;
150 let main_repo_path = get_main_repo_root().ok()?;
151 Some((worktree_path, main_repo_path))
152 } else {
153 None
154 }
155}
156
157fn cleanup_worktree(
159 worktree_path: &std::path::Path,
160 main_repo_path: &std::path::Path,
161 errors: &mut Vec<String>,
162) -> bool {
163 use crate::worktree::remove_worktree;
164
165 if let Err(e) = std::env::set_current_dir(main_repo_path) {
167 errors.push(format!(
168 "Failed to change to main repo '{}': {}",
169 main_repo_path.display(),
170 e
171 ));
172 return false;
173 }
174
175 if let Err(e) = remove_worktree(worktree_path, true) {
177 errors.push(format!(
178 "Failed to remove worktree '{}': {}",
179 worktree_path.display(),
180 e
181 ));
182 return false;
183 }
184
185 true
186}
187
188fn cleanup_test_file(errors: &mut Vec<String>) -> bool {
195 if let Ok(cwd) = std::env::current_dir() {
197 let test_file = cwd.join(SELF_TEST_FILE);
198 if test_file.exists() {
199 if let Err(e) = fs::remove_file(&test_file) {
200 errors.push(format!("Failed to delete {}: {}", test_file.display(), e));
201 return false;
202 }
203 return true;
204 }
205 }
206
207 let repo_root = match get_main_repo_root() {
209 Ok(root) => root,
210 Err(e) => {
211 errors.push(format!(
214 "Could not locate {}: not in CWD and failed to get repo root: {}",
215 SELF_TEST_FILE, e
216 ));
217 return false;
218 }
219 };
220
221 let test_file = repo_root.join(SELF_TEST_FILE);
222 if test_file.exists() {
223 if let Err(e) = fs::remove_file(&test_file) {
224 errors.push(format!("Failed to delete {}: {}", test_file.display(), e));
225 return false;
226 }
227 }
228 true
229}
230
231fn cleanup_spec_file(errors: &mut Vec<String>) -> bool {
233 let spec_path = match spec_dir() {
234 Ok(dir) => dir.join(SELF_TEST_SPEC_FILENAME),
235 Err(e) => {
236 errors.push(format!("Failed to get spec directory: {}", e));
237 return false;
238 }
239 };
240
241 if spec_path.exists() {
242 if let Err(e) = fs::remove_file(&spec_path) {
243 errors.push(format!("Failed to delete {}: {}", spec_path.display(), e));
244 return false;
245 }
246 }
247 true
248}
249
250fn cleanup_session_state(errors: &mut Vec<String>) -> bool {
252 let state_manager = match StateManager::new() {
253 Ok(sm) => sm,
254 Err(e) => {
255 errors.push(format!("Failed to create state manager: {}", e));
256 return false;
257 }
258 };
259
260 if let Err(e) = state_manager.clear_current() {
261 errors.push(format!("Failed to clear session state: {}", e));
262 return false;
263 }
264 true
265}
266
267fn cleanup_test_branch(errors: &mut Vec<String>) -> bool {
269 let current_branch = match get_current_branch() {
271 Ok(branch) => branch,
272 Err(e) => {
273 errors.push(format!("Failed to get current branch: {}", e));
274 return false;
275 }
276 };
277
278 if current_branch == SELF_TEST_BRANCH {
280 let base_branch = detect_base_branch_for_cleanup();
281 if let Err(e) = checkout_branch(&base_branch) {
282 errors.push(format!("Failed to checkout {}: {}", base_branch, e));
283 return false;
284 }
285 }
286
287 if branch_exists_local(SELF_TEST_BRANCH) {
289 if let Err(e) = delete_branch(SELF_TEST_BRANCH) {
290 errors.push(format!(
291 "Failed to delete branch '{}': {}",
292 SELF_TEST_BRANCH, e
293 ));
294 return false;
295 }
296 }
297 true
298}
299
300fn get_current_branch() -> Result<String> {
302 let output = Command::new("git")
303 .args(["rev-parse", "--abbrev-ref", "HEAD"])
304 .output()?;
305
306 if !output.status.success() {
307 return Err(crate::error::Autom8Error::GitError(
308 String::from_utf8_lossy(&output.stderr).to_string(),
309 ));
310 }
311
312 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
313}
314
315fn detect_base_branch_for_cleanup() -> String {
317 if branch_exists_local("main") {
319 "main".to_string()
320 } else if branch_exists_local("master") {
321 "master".to_string()
322 } else {
323 "main".to_string()
325 }
326}
327
328fn branch_exists_local(branch: &str) -> bool {
330 Command::new("git")
331 .args([
332 "show-ref",
333 "--verify",
334 "--quiet",
335 &format!("refs/heads/{}", branch),
336 ])
337 .output()
338 .map(|o| o.status.success())
339 .unwrap_or(false)
340}
341
342fn checkout_branch(branch: &str) -> Result<()> {
344 let output = Command::new("git").args(["checkout", branch]).output()?;
345
346 if !output.status.success() {
347 return Err(crate::error::Autom8Error::GitError(format!(
348 "Failed to checkout branch '{}': {}",
349 branch,
350 String::from_utf8_lossy(&output.stderr)
351 )));
352 }
353
354 Ok(())
355}
356
357fn delete_branch(branch: &str) -> Result<()> {
359 let output = Command::new("git")
360 .args(["branch", "-D", branch])
361 .output()?;
362
363 if !output.status.success() {
364 return Err(crate::error::Autom8Error::GitError(format!(
365 "Failed to delete branch '{}': {}",
366 branch,
367 String::from_utf8_lossy(&output.stderr)
368 )));
369 }
370
371 Ok(())
372}
373
374pub fn print_failure_details(run_error: &crate::error::Autom8Error) {
376 use crate::output::{print_error, print_warning};
377
378 println!(); print_error(&format!("Self-test failed: {}", run_error));
380
381 match run_error {
383 crate::error::Autom8Error::ClaudeError(msg) => {
384 print_warning(&format!("Claude error details: {}", msg));
385 }
386 crate::error::Autom8Error::ClaudeTimeout(secs) => {
387 print_warning(&format!("Claude timed out after {} seconds", secs));
388 }
389 crate::error::Autom8Error::MaxReviewIterationsReached => {
390 print_warning("Review failed after maximum iterations");
391 }
392 crate::error::Autom8Error::Interrupted => {
393 print_warning("Run was interrupted by user");
394 }
395 _ => {}
396 }
397}
398
399pub fn print_cleanup_results(result: &CleanupResult) {
401 use crate::output::{print_info, print_warning, GREEN, RESET};
402
403 println!(); print_info("Cleaning up self-test artifacts...");
405
406 if result.test_file_deleted {
407 println!(" {GREEN}✓{RESET} Deleted {}", SELF_TEST_FILE);
408 }
409 if result.spec_file_deleted {
410 println!(
411 " {GREEN}✓{RESET} Deleted spec file ({})",
412 SELF_TEST_SPEC_FILENAME
413 );
414 }
415 if result.session_cleared {
416 println!(" {GREEN}✓{RESET} Cleared session state");
417 }
418 if result.worktree_deleted {
419 println!(" {GREEN}✓{RESET} Removed worktree");
420 }
421 if result.branch_deleted {
422 println!(" {GREEN}✓{RESET} Deleted branch '{}'", SELF_TEST_BRANCH);
423 }
424
425 if !result.errors.is_empty() {
426 println!();
427 print_warning("Some cleanup operations failed:");
428 for error in &result.errors {
429 print_warning(&format!(" - {}", error));
430 }
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_create_self_test_spec_returns_valid_spec() {
440 let spec = create_self_test_spec();
441
442 assert_eq!(spec.project, "autom8-self-test");
443 assert_eq!(spec.branch_name, SELF_TEST_BRANCH);
444 assert!(!spec.description.is_empty());
445 }
446
447 #[test]
448 fn test_self_test_spec_has_three_stories() {
449 let spec = create_self_test_spec();
450
451 assert_eq!(spec.user_stories.len(), 3);
452 }
453
454 #[test]
455 fn test_self_test_spec_stories_are_not_passing() {
456 let spec = create_self_test_spec();
457
458 for story in &spec.user_stories {
459 assert!(
460 !story.passes,
461 "Story {} should not be passing initially",
462 story.id
463 );
464 }
465 }
466
467 #[test]
468 fn test_self_test_spec_stories_have_correct_priorities() {
469 let spec = create_self_test_spec();
470
471 assert_eq!(spec.user_stories[0].priority, 1);
472 assert_eq!(spec.user_stories[1].priority, 2);
473 assert_eq!(spec.user_stories[2].priority, 3);
474 }
475
476 #[test]
477 fn test_self_test_spec_stories_have_ids() {
478 let spec = create_self_test_spec();
479
480 assert_eq!(spec.user_stories[0].id, "ST-001");
481 assert_eq!(spec.user_stories[1].id, "ST-002");
482 assert_eq!(spec.user_stories[2].id, "ST-003");
483 }
484
485 #[test]
486 fn test_self_test_spec_stories_have_acceptance_criteria() {
487 let spec = create_self_test_spec();
488
489 for story in &spec.user_stories {
490 assert!(
491 !story.acceptance_criteria.is_empty(),
492 "Story {} should have acceptance criteria",
493 story.id
494 );
495 }
496 }
497
498 #[test]
499 fn test_self_test_spec_can_be_serialized_to_json() {
500 let spec = create_self_test_spec();
501
502 let json = serde_json::to_string_pretty(&spec);
503 assert!(json.is_ok(), "Spec should serialize to JSON");
504
505 let json_str = json.unwrap();
506 assert!(json_str.contains("autom8-self-test"));
507 assert!(json_str.contains("ST-001"));
508 assert!(json_str.contains("test_output.txt"));
509 }
510
511 #[test]
512 fn test_self_test_spec_round_trips_through_json() {
513 let spec = create_self_test_spec();
514
515 let json = serde_json::to_string(&spec).unwrap();
516 let parsed: Spec = serde_json::from_str(&json).unwrap();
517
518 assert_eq!(parsed.project, spec.project);
519 assert_eq!(parsed.branch_name, spec.branch_name);
520 assert_eq!(parsed.user_stories.len(), spec.user_stories.len());
521 }
522
523 #[test]
524 fn test_self_test_branch_constant() {
525 assert_eq!(SELF_TEST_BRANCH, "autom8/self-test");
526 }
527
528 #[test]
529 fn test_self_test_file_constant() {
530 assert_eq!(SELF_TEST_FILE, "test_output.txt");
531 }
532
533 #[test]
534 fn test_self_test_spec_filename_constant() {
535 assert_eq!(SELF_TEST_SPEC_FILENAME, "test_spec.json");
536 }
537
538 #[test]
543 fn test_cleanup_result_default_is_empty() {
544 let result = CleanupResult::default();
545
546 assert!(!result.test_file_deleted);
547 assert!(!result.spec_file_deleted);
548 assert!(!result.session_cleared);
549 assert!(!result.branch_deleted);
550 assert!(!result.worktree_deleted);
551 assert!(result.errors.is_empty());
552 }
553
554 #[test]
555 fn test_cleanup_result_is_complete_when_no_errors() {
556 let mut result = CleanupResult::default();
557 result.test_file_deleted = true;
558 result.spec_file_deleted = true;
559 result.session_cleared = true;
560 result.branch_deleted = true;
561 result.worktree_deleted = true;
562
563 assert!(result.is_complete());
564 }
565
566 #[test]
567 fn test_cleanup_result_is_not_complete_with_errors() {
568 let mut result = CleanupResult::default();
569 result.test_file_deleted = true;
570 result.errors.push("Failed to delete something".to_string());
571
572 assert!(!result.is_complete());
573 }
574
575 #[test]
576 fn test_cleanup_result_collects_multiple_errors() {
577 let mut result = CleanupResult::default();
578 result.errors.push("Error 1".to_string());
579 result.errors.push("Error 2".to_string());
580
581 assert_eq!(result.errors.len(), 2);
582 assert!(!result.is_complete());
583 }
584
585 #[test]
586 fn test_branch_exists_local_returns_bool() {
587 let exists = branch_exists_local("main");
590 assert!(exists || !exists);
592 }
593
594 #[test]
595 fn test_branch_exists_local_nonexistent_branch() {
596 let exists = branch_exists_local("nonexistent-branch-xyz-123456789");
598 assert!(!exists);
599 }
600
601 #[test]
602 fn test_detect_base_branch_for_cleanup_returns_string() {
603 let branch = detect_base_branch_for_cleanup();
605 assert!(!branch.is_empty());
606 assert!(
608 branch == "main" || branch == "master",
609 "Expected 'main' or 'master', got '{}'",
610 branch
611 );
612 }
613
614 #[test]
615 fn test_get_current_branch_returns_result() {
616 let result = get_current_branch();
618 assert!(result.is_ok(), "Should be able to get current branch");
619 let branch = result.unwrap();
620 assert!(!branch.is_empty(), "Branch name should not be empty");
621 }
622
623 #[test]
628 fn test_get_worktree_info_for_cleanup_returns_correct_value() {
629 use crate::worktree::is_in_worktree;
631
632 let info = get_worktree_info_for_cleanup();
633 let in_worktree = is_in_worktree().unwrap_or(false);
634
635 if in_worktree {
636 let (worktree_path, main_repo_path) =
638 info.expect("get_worktree_info_for_cleanup should return Some when in a worktree");
639 assert!(worktree_path.exists(), "worktree_path should exist");
640 assert!(main_repo_path.exists(), "main_repo_path should exist");
641 assert_ne!(
642 worktree_path, main_repo_path,
643 "worktree_path and main_repo_path should be different"
644 );
645 } else {
646 assert!(
648 info.is_none(),
649 "get_worktree_info_for_cleanup should return None when not in a worktree"
650 );
651 }
652 }
653
654 #[test]
655 fn test_cleanup_result_worktree_deleted_field() {
656 let mut result = CleanupResult::default();
657 assert!(
658 !result.worktree_deleted,
659 "worktree_deleted should default to false"
660 );
661
662 result.worktree_deleted = true;
663 assert!(
664 result.worktree_deleted,
665 "worktree_deleted should be settable to true"
666 );
667 }
668}