agent_diva_core/soul/
mod.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct SoulState {
10 pub bootstrap_seeded_at: Option<DateTime<Utc>>,
12 pub bootstrap_completed_at: Option<DateTime<Utc>>,
14}
15
16pub struct SoulStateStore {
18 path: PathBuf,
19}
20
21impl SoulStateStore {
22 pub fn new(workspace: impl AsRef<Path>) -> Self {
24 let path = workspace
25 .as_ref()
26 .join(".agent-diva")
27 .join("soul-state.json");
28 Self { path }
29 }
30
31 pub fn load(&self) -> std::io::Result<SoulState> {
33 if !self.path.exists() {
34 return Ok(SoulState::default());
35 }
36
37 let raw = std::fs::read_to_string(&self.path)?;
38 let state: SoulState = serde_json::from_str(&raw).unwrap_or_default();
39 Ok(state)
40 }
41
42 pub fn save(&self, state: &SoulState) -> std::io::Result<()> {
44 if let Some(parent) = self.path.parent() {
45 std::fs::create_dir_all(parent)?;
46 }
47 let content = serde_json::to_string_pretty(state)?;
48 std::fs::write(&self.path, content)
49 }
50
51 pub fn is_bootstrap_completed(&self) -> bool {
53 self.load()
54 .ok()
55 .and_then(|state| state.bootstrap_completed_at)
56 .is_some()
57 }
58
59 pub fn mark_bootstrap_seeded(&self) -> std::io::Result<()> {
61 let mut state = self.load().unwrap_or_default();
62 if state.bootstrap_seeded_at.is_none() {
63 state.bootstrap_seeded_at = Some(Utc::now());
64 self.save(&state)?;
65 }
66 Ok(())
67 }
68
69 pub fn mark_bootstrap_completed(&self) -> std::io::Result<()> {
71 let mut state = self.load().unwrap_or_default();
72 if state.bootstrap_seeded_at.is_none() {
73 state.bootstrap_seeded_at = Some(Utc::now());
74 }
75 if state.bootstrap_completed_at.is_none() {
76 state.bootstrap_completed_at = Some(Utc::now());
77 self.save(&state)?;
78 }
79 Ok(())
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn test_load_missing_returns_default() {
89 let temp = tempfile::tempdir().unwrap();
90 let store = SoulStateStore::new(temp.path());
91 let state = store.load().unwrap();
92 assert!(state.bootstrap_seeded_at.is_none());
93 assert!(state.bootstrap_completed_at.is_none());
94 }
95
96 #[test]
97 fn test_mark_bootstrap_seeded_writes_once() {
98 let temp = tempfile::tempdir().unwrap();
99 let store = SoulStateStore::new(temp.path());
100
101 store.mark_bootstrap_seeded().unwrap();
102 let first = store.load().unwrap();
103 assert!(first.bootstrap_seeded_at.is_some());
104
105 store.mark_bootstrap_seeded().unwrap();
106 let second = store.load().unwrap();
107 assert_eq!(first.bootstrap_seeded_at, second.bootstrap_seeded_at);
108 }
109
110 #[test]
111 fn test_is_bootstrap_completed() {
112 let temp = tempfile::tempdir().unwrap();
113 let store = SoulStateStore::new(temp.path());
114 assert!(!store.is_bootstrap_completed());
115
116 let mut state = SoulState::default();
117 state.bootstrap_completed_at = Some(Utc::now());
118 store.save(&state).unwrap();
119 assert!(store.is_bootstrap_completed());
120 }
121
122 #[test]
123 fn test_mark_bootstrap_completed_sets_completed_at() {
124 let temp = tempfile::tempdir().unwrap();
125 let store = SoulStateStore::new(temp.path());
126 store.mark_bootstrap_completed().unwrap();
127 let state = store.load().unwrap();
128 assert!(state.bootstrap_seeded_at.is_some());
129 assert!(state.bootstrap_completed_at.is_some());
130 }
131}