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#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ReopenResult {
15 pub unit: Unit,
16 pub path: PathBuf,
17}
18
19pub 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}