bamboo_server/app_state/
session_loader.rs1use super::*;
14
15use bamboo_agent_core::Session;
16use bamboo_engine::session_app::errors::{SessionLoadError, SessionSaveError};
17use bamboo_engine::session_app::repository::SessionAccess;
18
19#[async_trait::async_trait]
24impl SessionAccess for AppState {
25 async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
26 SessionAccess::load_session(&self.session_repo, id).await
27 }
28
29 async fn load_or_create(&self, id: &str, model: &str) -> Result<Session, SessionLoadError> {
30 SessionAccess::load_or_create(&self.session_repo, id, model).await
31 }
32
33 async fn load_merged(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
34 SessionAccess::load_merged(&self.session_repo, id).await
35 }
36
37 async fn save_session(&self, session: &mut Session) -> Result<(), SessionSaveError> {
38 SessionAccess::save_session(&self.session_repo, session).await
39 }
40
41 async fn save_and_cache(&self, session: &mut Session) -> Result<(), SessionSaveError> {
42 SessionAccess::save_and_cache(&self.session_repo, session).await
43 }
44}
45
46impl AppState {
51 pub async fn load_session(&self, session_id: &str) -> Option<bamboo_agent_core::Session> {
53 self.session_repo.load(session_id).await
54 }
55
56 pub async fn load_or_create_session(
58 &self,
59 session_id: &str,
60 model: &str,
61 ) -> bamboo_agent_core::Session {
62 self.session_repo.load_or_create(session_id, model).await
63 }
64
65 pub async fn load_session_merged(
67 &self,
68 session_id: &str,
69 ) -> Option<bamboo_agent_core::Session> {
70 self.session_repo.load_merged(session_id).await
71 }
72
73 pub async fn save_and_cache_session(&self, session: &mut bamboo_agent_core::Session) {
75 self.session_repo.save_and_cache(session).await
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[tokio::test]
84 async fn load_session_returns_from_memory_first() {
85 let temp_dir = tempfile::tempdir().expect("temp dir");
86 let state = AppState::new(temp_dir.path().to_path_buf())
87 .await
88 .expect("app state");
89
90 let session_id = "session-memory-first";
91 let session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
92
93 state.sessions.insert(
95 session_id.to_string(),
96 Arc::new(parking_lot::RwLock::new(session.clone())),
97 );
98
99 let loaded = state.load_session(session_id).await;
100 assert!(loaded.is_some());
101 assert_eq!(loaded.unwrap().id, session_id);
102 }
103
104 #[tokio::test]
105 async fn load_session_falls_back_to_storage() {
106 let temp_dir = tempfile::tempdir().expect("temp dir");
107 let state = AppState::new(temp_dir.path().to_path_buf())
108 .await
109 .expect("app state");
110
111 let session_id = "session-storage-fallback";
112 let session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
113
114 state
116 .storage
117 .save_session(&session)
118 .await
119 .expect("save session");
120
121 let loaded = state.load_session(session_id).await;
122 assert!(loaded.is_some());
123 assert_eq!(loaded.unwrap().id, session_id);
124 }
125
126 #[tokio::test]
127 async fn load_session_returns_none_when_missing() {
128 let temp_dir = tempfile::tempdir().expect("temp dir");
129 let state = AppState::new(temp_dir.path().to_path_buf())
130 .await
131 .expect("app state");
132
133 let loaded = state.load_session("nonexistent").await;
134 assert!(loaded.is_none());
135 }
136
137 #[tokio::test]
138 async fn load_or_create_creates_new_when_missing() {
139 let temp_dir = tempfile::tempdir().expect("temp dir");
140 let state = AppState::new(temp_dir.path().to_path_buf())
141 .await
142 .expect("app state");
143
144 let session = state.load_or_create_session("new-session", "gpt-4").await;
145 assert_eq!(session.id, "new-session");
146 assert_eq!(session.model, "gpt-4");
147 }
148
149 #[tokio::test]
150 async fn load_session_merged_prefers_storage_with_pending_question() {
151 let temp_dir = tempfile::tempdir().expect("temp dir");
152 let state = AppState::new(temp_dir.path().to_path_buf())
153 .await
154 .expect("app state");
155
156 let session_id = "session-merge-pending";
157 let memory_session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
158 let mut storage_session = memory_session.clone();
159 storage_session.set_pending_question(
160 "tool-call-1".to_string(),
161 "ConclusionWithOptions".to_string(),
162 "Need confirmation?".to_string(),
163 vec!["OK".to_string()],
164 true,
165 );
166
167 state.sessions.insert(
168 session_id.to_string(),
169 Arc::new(parking_lot::RwLock::new(memory_session)),
170 );
171 state
172 .storage
173 .save_session(&storage_session)
174 .await
175 .expect("save session");
176
177 let loaded = state.load_session_merged(session_id).await;
178 assert!(loaded.is_some());
179 assert!(loaded.unwrap().pending_question.is_some());
180 }
181
182 #[tokio::test]
183 async fn save_and_cache_session_writes_both() {
184 let temp_dir = tempfile::tempdir().expect("temp dir");
185 let state = AppState::new(temp_dir.path().to_path_buf())
186 .await
187 .expect("app state");
188
189 let session_id = "session-save-cache";
190 let mut session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
191 session.title = "test-title".to_string();
192
193 state.save_and_cache_session(&mut session).await;
194
195 let cached = { bamboo_engine::read_cached_session(&state.sessions, session_id) };
197 assert!(cached.is_some());
198 assert_eq!(cached.unwrap().title, "test-title");
199
200 let loaded = state.storage.load_session(session_id).await;
202 assert!(loaded.is_ok());
203 assert_eq!(loaded.unwrap().unwrap().title, "test-title");
204 }
205}