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
10pub 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 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 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}