Skip to main content

mana_core/ops/
reopen.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result};
4use chrono::Utc;
5
6use serde::{Deserialize, Serialize};
7
8use crate::discovery::find_unit_file;
9use crate::index::Index;
10use crate::unit::{Status, Unit};
11
12/// Result of reopening a unit.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ReopenResult {
15    pub unit: Unit,
16    pub path: PathBuf,
17}
18
19/// Reopen a closed unit.
20pub fn reopen(mana_dir: &Path, id: &str) -> Result<ReopenResult> {
21    let unit_path =
22        find_unit_file(mana_dir, id).with_context(|| format!("Unit not found: {}", id))?;
23    let mut unit =
24        Unit::from_file(&unit_path).with_context(|| format!("Failed to load unit: {}", id))?;
25
26    unit.status = Status::Open;
27    unit.closed_at = None;
28    unit.close_reason = None;
29    unit.updated_at = Utc::now();
30
31    unit.to_file(&unit_path)
32        .with_context(|| format!("Failed to save unit: {}", id))?;
33
34    let index = Index::build(mana_dir)?;
35    index.save(mana_dir)?;
36
37    Ok(ReopenResult {
38        unit,
39        path: unit_path,
40    })
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::ops::create::{self, tests::minimal_params};
47    use std::fs;
48    use tempfile::TempDir;
49
50    fn setup() -> (TempDir, PathBuf) {
51        let dir = TempDir::new().unwrap();
52        let bd = dir.path().join(".mana");
53        fs::create_dir(&bd).unwrap();
54        crate::config::Config {
55            project: "test".to_string(),
56            next_id: 1,
57            auto_close_parent: true,
58            run: None,
59            plan: None,
60            max_loops: 10,
61            max_concurrent: 4,
62            poll_interval: 30,
63            extends: vec![],
64            rules_file: None,
65            file_locking: false,
66            worktree: false,
67            on_close: None,
68            on_fail: None,
69            verify_timeout: None,
70            review: None,
71            user: None,
72            user_email: None,
73            auto_commit: false,
74            commit_template: None,
75            research: None,
76            run_model: None,
77            plan_model: None,
78            review_model: None,
79            research_model: None,
80            batch_verify: false,
81            memory_reserve_mb: 0,
82            notify: None,
83        }
84        .save(&bd)
85        .unwrap();
86        (dir, bd)
87    }
88
89    #[test]
90    fn reopen_closed_unit() {
91        let (_dir, bd) = setup();
92        create::create(&bd, minimal_params("Task")).unwrap();
93        let bp = find_unit_file(&bd, "1").unwrap();
94        let mut unit = Unit::from_file(&bp).unwrap();
95        unit.status = Status::Closed;
96        unit.closed_at = Some(Utc::now());
97        unit.close_reason = Some("Done".into());
98        unit.to_file(&bp).unwrap();
99
100        let r = reopen(&bd, "1").unwrap();
101        assert_eq!(r.unit.status, Status::Open);
102        assert!(r.unit.closed_at.is_none());
103        assert!(r.unit.close_reason.is_none());
104    }
105
106    #[test]
107    fn reopen_nonexistent() {
108        let (_dir, bd) = setup();
109        assert!(reopen(&bd, "99").is_err());
110    }
111
112    #[test]
113    fn reopen_rebuilds_index() {
114        let (_dir, bd) = setup();
115        create::create(&bd, minimal_params("Task")).unwrap();
116        let bp = find_unit_file(&bd, "1").unwrap();
117        let mut unit = Unit::from_file(&bp).unwrap();
118        unit.status = Status::Closed;
119        unit.to_file(&bp).unwrap();
120
121        reopen(&bd, "1").unwrap();
122        let index = Index::load(&bd).unwrap();
123        assert_eq!(
124            index.units.iter().find(|e| e.id == "1").unwrap().status,
125            Status::Open
126        );
127    }
128}