Skip to main content

mana/commands/
delete.rs

1use std::path::Path;
2
3use anyhow::Result;
4use mana_core::ops::delete as ops_delete;
5
6/// Delete a unit and clean up all references to it in other units' dependencies.
7///
8/// 1. Load the unit to get its title (for printing)
9/// 2. Delete the unit file
10/// 3. Scan all remaining units and remove deleted_id from their dependencies
11/// 4. Rebuild the index
12pub fn cmd_delete(mana_dir: &Path, id: &str) -> Result<()> {
13    let result = ops_delete::delete(mana_dir, id)?;
14    println!("Deleted unit {}: {}", result.id, result.title);
15    Ok(())
16}
17
18#[cfg(test)]
19mod tests {
20    use super::*;
21    use std::fs;
22
23    use crate::index::Index;
24    use crate::unit::Unit;
25    use crate::util::title_to_slug;
26    use tempfile::TempDir;
27
28    fn setup_test_mana_dir() -> (TempDir, std::path::PathBuf) {
29        let dir = TempDir::new().unwrap();
30        let mana_dir = dir.path().join(".mana");
31        fs::create_dir(&mana_dir).unwrap();
32        (dir, mana_dir)
33    }
34
35    #[test]
36    fn test_delete_unit() {
37        let (_dir, mana_dir) = setup_test_mana_dir();
38        let unit = Unit::new("1", "Task to delete");
39        let slug = title_to_slug(&unit.title);
40        let unit_path = mana_dir.join(format!("1-{}.md", slug));
41        unit.to_file(&unit_path).unwrap();
42
43        assert!(unit_path.exists());
44
45        cmd_delete(&mana_dir, "1").unwrap();
46
47        assert!(!unit_path.exists());
48    }
49
50    #[test]
51    fn test_delete_nonexistent_unit() {
52        let (_dir, mana_dir) = setup_test_mana_dir();
53        let result = cmd_delete(&mana_dir, "99");
54        assert!(result.is_err());
55    }
56
57    #[test]
58    fn test_delete_cleans_dependencies() {
59        let (_dir, mana_dir) = setup_test_mana_dir();
60
61        // Create units with dependencies
62        let unit1 = Unit::new("1", "Task 1");
63        let mut unit2 = Unit::new("2", "Task 2");
64        let mut unit3 = Unit::new("3", "Task 3");
65
66        unit2.dependencies = vec!["1".to_string()];
67        unit3.dependencies = vec!["1".to_string(), "2".to_string()];
68
69        let slug1 = title_to_slug(&unit1.title);
70        let slug2 = title_to_slug(&unit2.title);
71        let slug3 = title_to_slug(&unit3.title);
72
73        unit1
74            .to_file(mana_dir.join(format!("1-{}.md", slug1)))
75            .unwrap();
76        unit2
77            .to_file(mana_dir.join(format!("2-{}.md", slug2)))
78            .unwrap();
79        unit3
80            .to_file(mana_dir.join(format!("3-{}.md", slug3)))
81            .unwrap();
82
83        // Delete unit 1
84        cmd_delete(&mana_dir, "1").unwrap();
85
86        // Verify unit 2 no longer depends on 1
87        let unit2_updated =
88            Unit::from_file(crate::discovery::find_unit_file(&mana_dir, "2").unwrap()).unwrap();
89        assert!(!unit2_updated.dependencies.contains(&"1".to_string()));
90
91        // Verify unit 3 no longer depends on 1, but still depends on 2
92        let unit3_updated =
93            Unit::from_file(crate::discovery::find_unit_file(&mana_dir, "3").unwrap()).unwrap();
94        assert!(!unit3_updated.dependencies.contains(&"1".to_string()));
95        assert!(unit3_updated.dependencies.contains(&"2".to_string()));
96    }
97
98    #[test]
99    fn test_delete_rebuilds_index() {
100        let (_dir, mana_dir) = setup_test_mana_dir();
101        let unit1 = Unit::new("1", "Task 1");
102        let unit2 = Unit::new("2", "Task 2");
103        let slug1 = title_to_slug(&unit1.title);
104        let slug2 = title_to_slug(&unit2.title);
105        unit1
106            .to_file(mana_dir.join(format!("1-{}.md", slug1)))
107            .unwrap();
108        unit2
109            .to_file(mana_dir.join(format!("2-{}.md", slug2)))
110            .unwrap();
111
112        cmd_delete(&mana_dir, "1").unwrap();
113
114        let index = Index::load(&mana_dir).unwrap();
115        assert_eq!(index.units.len(), 1);
116        assert_eq!(index.units[0].id, "2");
117    }
118
119    #[test]
120    fn test_cleanup_does_not_modify_unrelated_units() {
121        let (_dir, mana_dir) = setup_test_mana_dir();
122
123        // Create units where only some depend on 1
124        let unit1 = Unit::new("1", "Task 1");
125        let mut unit2 = Unit::new("2", "Task 2");
126        let unit3 = Unit::new("3", "Task 3"); // No dependencies
127
128        unit2.dependencies = vec!["1".to_string()];
129
130        let slug1 = title_to_slug(&unit1.title);
131        let slug2 = title_to_slug(&unit2.title);
132        let slug3 = title_to_slug(&unit3.title);
133
134        unit1
135            .to_file(mana_dir.join(format!("1-{}.md", slug1)))
136            .unwrap();
137        unit2
138            .to_file(mana_dir.join(format!("2-{}.md", slug2)))
139            .unwrap();
140        unit3
141            .to_file(mana_dir.join(format!("3-{}.md", slug3)))
142            .unwrap();
143
144        cmd_delete(&mana_dir, "1").unwrap();
145
146        let unit3_check =
147            Unit::from_file(crate::discovery::find_unit_file(&mana_dir, "3").unwrap()).unwrap();
148        assert!(unit3_check.dependencies.is_empty());
149    }
150
151    #[test]
152    fn test_delete_with_complex_dependency_graph() {
153        let (_dir, mana_dir) = setup_test_mana_dir();
154
155        // Create a diamond dependency: 4 <- [2, 3], 2 <- 1, 3 <- 1
156        let unit1 = Unit::new("1", "Root");
157        let mut unit2 = Unit::new("2", "Middle left");
158        let mut unit3 = Unit::new("3", "Middle right");
159        let mut unit4 = Unit::new("4", "Bottom");
160
161        unit2.dependencies = vec!["1".to_string()];
162        unit3.dependencies = vec!["1".to_string()];
163        unit4.dependencies = vec!["2".to_string(), "3".to_string()];
164
165        let slug1 = title_to_slug(&unit1.title);
166        let slug2 = title_to_slug(&unit2.title);
167        let slug3 = title_to_slug(&unit3.title);
168        let slug4 = title_to_slug(&unit4.title);
169
170        unit1
171            .to_file(mana_dir.join(format!("1-{}.md", slug1)))
172            .unwrap();
173        unit2
174            .to_file(mana_dir.join(format!("2-{}.md", slug2)))
175            .unwrap();
176        unit3
177            .to_file(mana_dir.join(format!("3-{}.md", slug3)))
178            .unwrap();
179        unit4
180            .to_file(mana_dir.join(format!("4-{}.md", slug4)))
181            .unwrap();
182
183        // Delete node 1
184        cmd_delete(&mana_dir, "1").unwrap();
185
186        // Verify cleanup
187        let unit2_updated =
188            Unit::from_file(crate::discovery::find_unit_file(&mana_dir, "2").unwrap()).unwrap();
189        let unit3_updated =
190            Unit::from_file(crate::discovery::find_unit_file(&mana_dir, "3").unwrap()).unwrap();
191        let unit4_updated =
192            Unit::from_file(crate::discovery::find_unit_file(&mana_dir, "4").unwrap()).unwrap();
193
194        assert!(!unit2_updated.dependencies.contains(&"1".to_string()));
195        assert!(!unit3_updated.dependencies.contains(&"1".to_string()));
196        assert!(!unit4_updated.dependencies.contains(&"1".to_string()));
197        assert!(unit4_updated.dependencies.contains(&"2".to_string()));
198        assert!(unit4_updated.dependencies.contains(&"3".to_string()));
199    }
200
201    #[test]
202    fn test_delete_ignores_excluded_files() {
203        let (_dir, mana_dir) = setup_test_mana_dir();
204
205        let unit1 = Unit::new("1", "Task 1");
206        let slug1 = title_to_slug(&unit1.title);
207        unit1
208            .to_file(mana_dir.join(format!("1-{}.md", slug1)))
209            .unwrap();
210
211        // Create config.yaml with a fake reference to "1"
212        fs::write(
213            mana_dir.join("config.yaml"),
214            "next_id: 2\nproject_name: test\n",
215        )
216        .unwrap();
217
218        // This should not fail even though config.yaml exists
219        cmd_delete(&mana_dir, "1").unwrap();
220        assert!(!mana_dir.join(format!("1-{}.md", slug1)).exists());
221    }
222}