Skip to main content

mana_core/ops/
delete.rs

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/// Result of deleting a unit.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DeleteResult {
16    pub id: String,
17    pub title: String,
18}
19
20/// Delete a unit and clean up dependency references.
21pub 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}