atman_runtime/memory/
goal.rs1use 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}