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