Skip to main content

bn/commands/
reopen.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4use chrono::Utc;
5
6use crate::bean::Bean;
7use crate::discovery::find_bean_file;
8use crate::index::Index;
9
10/// Reopen a closed bean.
11///
12/// Sets status=open, clears closed_at and close_reason.
13/// Updates updated_at and rebuilds index.
14pub fn cmd_reopen(beans_dir: &Path, id: &str) -> Result<()> {
15    let bean_path =
16        find_bean_file(beans_dir, id).with_context(|| format!("Bean not found: {}", id))?;
17
18    let mut bean =
19        Bean::from_file(&bean_path).with_context(|| format!("Failed to load bean: {}", id))?;
20
21    bean.status = crate::bean::Status::Open;
22    bean.closed_at = None;
23    bean.close_reason = None;
24    bean.updated_at = Utc::now();
25
26    bean.to_file(&bean_path)
27        .with_context(|| format!("Failed to save bean: {}", id))?;
28
29    // Rebuild index
30    let index = Index::build(beans_dir).with_context(|| "Failed to rebuild index")?;
31    index
32        .save(beans_dir)
33        .with_context(|| "Failed to save index")?;
34
35    println!("Reopened bean {}: {}", id, bean.title);
36    Ok(())
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use crate::bean::Status;
43    use crate::util::title_to_slug;
44    use std::fs;
45    use tempfile::TempDir;
46
47    fn setup_test_beans_dir() -> (TempDir, std::path::PathBuf) {
48        let dir = TempDir::new().unwrap();
49        let beans_dir = dir.path().join(".beans");
50        fs::create_dir(&beans_dir).unwrap();
51        (dir, beans_dir)
52    }
53
54    #[test]
55    fn test_reopen_closed_bean() {
56        let (_dir, beans_dir) = setup_test_beans_dir();
57        let mut bean = Bean::new("1", "Task");
58        bean.status = Status::Closed;
59        bean.closed_at = Some(Utc::now());
60        bean.close_reason = Some("Done".to_string());
61        let slug = title_to_slug(&bean.title);
62        bean.to_file(beans_dir.join(format!("1-{}.md", slug)))
63            .unwrap();
64
65        cmd_reopen(&beans_dir, "1").unwrap();
66
67        let reopened =
68            Bean::from_file(crate::discovery::find_bean_file(&beans_dir, "1").unwrap()).unwrap();
69        assert_eq!(reopened.status, Status::Open);
70        assert!(reopened.closed_at.is_none());
71        assert!(reopened.close_reason.is_none());
72    }
73
74    #[test]
75    fn test_reopen_nonexistent_bean() {
76        let (_dir, beans_dir) = setup_test_beans_dir();
77        let result = cmd_reopen(&beans_dir, "99");
78        assert!(result.is_err());
79    }
80
81    #[test]
82    fn test_reopen_updates_updated_at() {
83        let (_dir, beans_dir) = setup_test_beans_dir();
84        let mut bean = Bean::new("1", "Task");
85        bean.status = Status::Closed;
86        bean.closed_at = Some(Utc::now());
87        let original_updated_at = bean.updated_at;
88        let slug = title_to_slug(&bean.title);
89        bean.to_file(beans_dir.join(format!("1-{}.md", slug)))
90            .unwrap();
91
92        std::thread::sleep(std::time::Duration::from_millis(10));
93
94        cmd_reopen(&beans_dir, "1").unwrap();
95
96        let reopened =
97            Bean::from_file(crate::discovery::find_bean_file(&beans_dir, "1").unwrap()).unwrap();
98        assert!(reopened.updated_at > original_updated_at);
99    }
100
101    #[test]
102    fn test_reopen_rebuilds_index() {
103        let (_dir, beans_dir) = setup_test_beans_dir();
104        let mut bean = Bean::new("1", "Task");
105        bean.status = Status::Closed;
106        let slug = title_to_slug(&bean.title);
107        bean.to_file(beans_dir.join(format!("1-{}.md", slug)))
108            .unwrap();
109
110        cmd_reopen(&beans_dir, "1").unwrap();
111
112        let index = Index::load(&beans_dir).unwrap();
113        let entry = index.beans.iter().find(|e| e.id == "1").unwrap();
114        assert_eq!(entry.status, Status::Open);
115    }
116
117    #[test]
118    fn test_reopen_open_bean() {
119        let (_dir, beans_dir) = setup_test_beans_dir();
120        let bean = Bean::new("1", "Task");
121        let slug = title_to_slug(&bean.title);
122        bean.to_file(beans_dir.join(format!("1-{}.md", slug)))
123            .unwrap();
124
125        // Should work fine even if already open
126        cmd_reopen(&beans_dir, "1").unwrap();
127
128        let reopened =
129            Bean::from_file(crate::discovery::find_bean_file(&beans_dir, "1").unwrap()).unwrap();
130        assert_eq!(reopened.status, Status::Open);
131    }
132}