Skip to main content

atman_runtime/memory/
goal.rs

1use std::path::{Path, PathBuf};
2
3pub struct GoalStore {
4    path: PathBuf,
5    notify: Option<tokio::sync::watch::Sender<Option<String>>>,
6}
7
8impl GoalStore {
9    pub fn at(session_dir: impl AsRef<Path>) -> Self {
10        Self {
11            path: session_dir.as_ref().join("goal.txt"),
12            notify: None,
13        }
14    }
15
16    pub fn with_notify(mut self, tx: tokio::sync::watch::Sender<Option<String>>) -> Self {
17        self.notify = Some(tx);
18        self
19    }
20
21    pub fn path(&self) -> &Path {
22        &self.path
23    }
24
25    pub fn get(&self) -> std::io::Result<String> {
26        match std::fs::read_to_string(&self.path) {
27            Ok(s) => Ok(s.trim_end().to_string()),
28            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
29            Err(e) => Err(e),
30        }
31    }
32
33    pub fn set(&self, text: &str) -> std::io::Result<()> {
34        if let Some(parent) = self.path.parent()
35            && !parent.as_os_str().is_empty()
36        {
37            std::fs::create_dir_all(parent)?;
38        }
39        std::fs::write(&self.path, text.trim_end())?;
40        if let Some(tx) = &self.notify {
41            let _ = tx.send(Some(text.trim_end().to_string()));
42        }
43        Ok(())
44    }
45
46    pub fn clear(&self) -> std::io::Result<()> {
47        match std::fs::remove_file(&self.path) {
48            Ok(()) => {
49                if let Some(tx) = &self.notify {
50                    let _ = tx.send(None);
51                }
52                Ok(())
53            }
54            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
55            Err(e) => Err(e),
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn empty_when_never_set() {
66        let tmp = tempfile::tempdir().unwrap();
67        let g = GoalStore::at(tmp.path());
68        assert_eq!(g.get().unwrap(), "");
69    }
70
71    #[test]
72    fn set_then_get_roundtrips_trimmed() {
73        let tmp = tempfile::tempdir().unwrap();
74        let g = GoalStore::at(tmp.path());
75        g.set("ship an atman agent\n\n").unwrap();
76        assert_eq!(g.get().unwrap(), "ship an atman agent");
77    }
78
79    #[test]
80    fn set_overwrites_previous() {
81        let tmp = tempfile::tempdir().unwrap();
82        let g = GoalStore::at(tmp.path());
83        g.set("v1").unwrap();
84        g.set("v2").unwrap();
85        assert_eq!(g.get().unwrap(), "v2");
86    }
87
88    #[test]
89    fn clear_removes_file_and_get_returns_empty() {
90        let tmp = tempfile::tempdir().unwrap();
91        let g = GoalStore::at(tmp.path());
92        g.set("temporary").unwrap();
93        g.clear().unwrap();
94        assert_eq!(g.get().unwrap(), "");
95        assert!(!g.path().exists());
96    }
97
98    #[test]
99    fn clear_on_missing_file_is_ok() {
100        let tmp = tempfile::tempdir().unwrap();
101        let g = GoalStore::at(tmp.path());
102        g.clear().unwrap();
103    }
104
105    #[test]
106    fn set_creates_parent_dir() {
107        let tmp = tempfile::tempdir().unwrap();
108        let nested = tmp.path().join("nested/deep");
109        let g = GoalStore::at(&nested);
110        g.set("hi").unwrap();
111        assert_eq!(g.get().unwrap(), "hi");
112    }
113}