1use std::fs;
2use std::path::Path;
3
4use anyhow::Context;
5use anyhow::Result;
6
7use serde::{Deserialize, Serialize};
8
9use crate::discovery::find_unit_file;
10use crate::index::Index;
11use crate::unit::Unit;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DeleteResult {
16 pub id: String,
17 pub title: String,
18}
19
20pub fn delete(mana_dir: &Path, id: &str) -> Result<DeleteResult> {
22 let unit_path =
23 find_unit_file(mana_dir, id).with_context(|| format!("Unit not found: {}", id))?;
24 let unit =
25 Unit::from_file(&unit_path).with_context(|| format!("Failed to load unit: {}", id))?;
26 let title = unit.title.clone();
27 fs::remove_file(&unit_path).with_context(|| format!("Failed to delete: {}", id))?;
28 cleanup_dep_references(mana_dir, id)?;
29 let index = Index::build(mana_dir)?;
30 index.save(mana_dir)?;
31 Ok(DeleteResult {
32 id: id.to_string(),
33 title,
34 })
35}
36
37fn cleanup_dep_references(mana_dir: &Path, deleted_id: &str) -> Result<()> {
38 let dir_entries = fs::read_dir(mana_dir)
39 .with_context(|| format!("Failed to read: {}", mana_dir.display()))?;
40 for entry in dir_entries {
41 let entry = entry?;
42 let path = entry.path();
43 let filename = path
44 .file_name()
45 .and_then(|n| n.to_str())
46 .unwrap_or_default();
47 if filename == "index.yaml" || filename == "config.yaml" || filename == "unit.yaml" {
48 continue;
49 }
50 let ext = path.extension().and_then(|e| e.to_str());
51 let is_unit = match ext {
52 Some("md") => filename.contains('-'),
53 Some("yaml") => true,
54 _ => false,
55 };
56 if !is_unit {
57 continue;
58 }
59 if let Ok(mut unit) = Unit::from_file(&path) {
60 let n = unit.dependencies.len();
61 unit.dependencies.retain(|d| d != deleted_id);
62 if unit.dependencies.len() < n {
63 unit.to_file(&path)?;
64 }
65 }
66 }
67 Ok(())
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::ops::create::{self, tests::minimal_params};
74 use tempfile::TempDir;
75
76 fn setup() -> (TempDir, std::path::PathBuf) {
77 let dir = TempDir::new().unwrap();
78 let bd = dir.path().join(".mana");
79 fs::create_dir(&bd).unwrap();
80 crate::config::Config {
81 project: "test".into(),
82 next_id: 1,
83 auto_close_parent: true,
84 run: None,
85 plan: None,
86 max_loops: 10,
87 max_concurrent: 4,
88 poll_interval: 30,
89 extends: vec![],
90 rules_file: None,
91 file_locking: false,
92 worktree: false,
93 on_close: None,
94 on_fail: None,
95 verify_timeout: None,
96 review: None,
97 user: None,
98 user_email: None,
99 auto_commit: false,
100 commit_template: None,
101 research: None,
102 run_model: None,
103 plan_model: None,
104 review_model: None,
105 research_model: None,
106 batch_verify: false,
107 memory_reserve_mb: 0,
108 notify: None,
109 }
110 .save(&bd)
111 .unwrap();
112 (dir, bd)
113 }
114
115 #[test]
116 fn delete_unit() {
117 let (_dir, bd) = setup();
118 let c = create::create(&bd, minimal_params("Task")).unwrap();
119 assert!(c.path.exists());
120 let r = delete(&bd, "1").unwrap();
121 assert_eq!(r.title, "Task");
122 assert!(!c.path.exists());
123 }
124
125 #[test]
126 fn delete_nonexistent() {
127 let (_dir, bd) = setup();
128 assert!(delete(&bd, "99").is_err());
129 }
130
131 #[test]
132 fn delete_cleans_deps() {
133 let (_dir, bd) = setup();
134 create::create(&bd, minimal_params("A")).unwrap();
135 let mut p = minimal_params("B");
136 p.dependencies = vec!["1".into()];
137 create::create(&bd, p).unwrap();
138 delete(&bd, "1").unwrap();
139 let b2 = Unit::from_file(find_unit_file(&bd, "2").unwrap()).unwrap();
140 assert!(!b2.dependencies.contains(&"1".to_string()));
141 }
142
143 #[test]
144 fn delete_rebuilds_index() {
145 let (_dir, bd) = setup();
146 create::create(&bd, minimal_params("A")).unwrap();
147 create::create(&bd, minimal_params("B")).unwrap();
148 delete(&bd, "1").unwrap();
149 let index = Index::load(&bd).unwrap();
150 assert_eq!(index.units.len(), 1);
151 }
152}