1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use mixtape_core::session::{
4 MessageRole, Session, SessionError, SessionMessage, SessionStore, SessionSummary, ToolCall,
5 ToolResult,
6};
7use rusqlite::{params, Connection, OptionalExtension};
8use std::path::PathBuf;
9use std::sync::{Arc, Mutex};
10
11pub struct SqliteStore {
28 conn: Arc<Mutex<Connection>>,
29}
30
31impl SqliteStore {
32 pub fn new(path: impl Into<PathBuf>) -> Result<Self, SessionError> {
37 let path = path.into();
38
39 if let Some(parent) = path.parent() {
41 std::fs::create_dir_all(parent)
42 .map_err(|e| SessionError::Storage(format!("Failed to create directory: {}", e)))?;
43 }
44
45 let conn = Connection::open(&path)
46 .map_err(|e| SessionError::Storage(format!("Failed to open database: {}", e)))?;
47
48 conn.execute_batch(include_str!("schema.sql"))
50 .map_err(|e| SessionError::Storage(format!("Failed to initialize schema: {}", e)))?;
51
52 Ok(Self {
53 conn: Arc::new(Mutex::new(conn)),
54 })
55 }
56
57 pub fn default_location() -> Result<Self, SessionError> {
59 Self::new(".mixtape/sessions.db")
60 }
61}
62
63#[async_trait]
64impl SessionStore for SqliteStore {
65 async fn get_or_create_session(&self) -> Result<Session, SessionError> {
66 let current_dir = std::env::current_dir()
67 .map_err(|e| SessionError::Storage(format!("Failed to get current directory: {}", e)))?
68 .display()
69 .to_string();
70
71 let existing_id: Option<String> = {
72 let conn = self.conn.lock().unwrap();
73
74 conn.query_row(
76 "SELECT id FROM sessions WHERE directory = ? ORDER BY updated_at DESC LIMIT 1",
77 params![current_dir],
78 |row| row.get::<_, String>(0),
79 )
80 .optional()
81 .map_err(|e| SessionError::Storage(e.to_string()))?
82 };
83
84 if let Some(id) = existing_id {
85 self.get_session(&id)
87 .await?
88 .ok_or_else(|| SessionError::NotFound(id.clone()))
89 } else {
90 let now = Utc::now();
92 let id = uuid::Uuid::new_v4().to_string();
93
94 {
95 let conn = self.conn.lock().unwrap();
96 conn.execute(
97 "INSERT INTO sessions (id, directory, created_at, updated_at) VALUES (?, ?, ?, ?)",
98 params![id, current_dir, now.timestamp(), now.timestamp()],
99 )
100 .map_err(|e| SessionError::Storage(e.to_string()))?;
101 }
102
103 Ok(Session {
104 id,
105 created_at: now,
106 updated_at: now,
107 directory: current_dir,
108 messages: Vec::new(),
109 })
110 }
111 }
112
113 async fn get_session(&self, id: &str) -> Result<Option<Session>, SessionError> {
114 let conn = self.conn.lock().unwrap();
115
116 let session_row = conn
118 .query_row(
119 "SELECT id, directory, created_at, updated_at FROM sessions WHERE id = ?",
120 params![id],
121 |row| {
122 Ok((
123 row.get::<_, String>(0)?,
124 row.get::<_, String>(1)?,
125 row.get::<_, i64>(2)?,
126 row.get::<_, i64>(3)?,
127 ))
128 },
129 )
130 .optional()
131 .map_err(|e| SessionError::Storage(e.to_string()))?;
132
133 let Some((id, directory, created_at, updated_at)) = session_row else {
134 return Ok(None);
135 };
136
137 let mut stmt = conn
139 .prepare(
140 "SELECT role, content, tool_calls, tool_results, timestamp
141 FROM messages WHERE session_id = ? ORDER BY idx",
142 )
143 .map_err(|e| SessionError::Storage(e.to_string()))?;
144
145 let messages = stmt
146 .query_map(params![id], |row| {
147 Ok((
148 row.get::<_, String>(0)?,
149 row.get::<_, String>(1)?,
150 row.get::<_, String>(2)?,
151 row.get::<_, String>(3)?,
152 row.get::<_, i64>(4)?,
153 ))
154 })
155 .map_err(|e| SessionError::Storage(e.to_string()))?
156 .collect::<Result<Vec<_>, _>>()
157 .map_err(|e| SessionError::Storage(e.to_string()))?
158 .into_iter()
159 .map(
160 |(role, content, tool_calls_json, tool_results_json, timestamp)| {
161 let role = match role.as_str() {
162 "User" => MessageRole::User,
163 "Assistant" => MessageRole::Assistant,
164 "System" => MessageRole::System,
165 _ => MessageRole::User,
166 };
167
168 let tool_calls: Vec<ToolCall> =
169 serde_json::from_str(&tool_calls_json).unwrap_or_default();
170 let tool_results: Vec<ToolResult> =
171 serde_json::from_str(&tool_results_json).unwrap_or_default();
172
173 SessionMessage {
174 role,
175 content,
176 tool_calls,
177 tool_results,
178 timestamp: DateTime::from_timestamp(timestamp, 0).unwrap_or(Utc::now()),
179 }
180 },
181 )
182 .collect();
183
184 Ok(Some(Session {
185 id,
186 created_at: DateTime::from_timestamp(created_at, 0).unwrap_or(Utc::now()),
187 updated_at: DateTime::from_timestamp(updated_at, 0).unwrap_or(Utc::now()),
188 directory,
189 messages,
190 }))
191 }
192
193 async fn save_session(&self, session: &Session) -> Result<(), SessionError> {
194 let mut conn = self.conn.lock().unwrap();
195
196 let tx = conn
199 .transaction()
200 .map_err(|e| SessionError::Storage(format!("Failed to begin transaction: {}", e)))?;
201
202 let now = Utc::now();
204 let rows = tx
205 .execute(
206 "UPDATE sessions SET updated_at = ? WHERE id = ?",
207 params![now.timestamp(), session.id],
208 )
209 .map_err(|e| SessionError::Storage(format!("Failed to update session: {}", e)))?;
210
211 if rows == 0 {
213 return Err(SessionError::NotFound(session.id.clone()));
214 }
215
216 tx.execute(
218 "DELETE FROM messages WHERE session_id = ?",
219 params![session.id],
220 )
221 .map_err(|e| SessionError::Storage(format!("Failed to delete old messages: {}", e)))?;
222
223 for (idx, msg) in session.messages.iter().enumerate() {
225 let tool_calls_json =
226 serde_json::to_string(&msg.tool_calls).map_err(SessionError::Serialization)?;
227 let tool_results_json =
228 serde_json::to_string(&msg.tool_results).map_err(SessionError::Serialization)?;
229
230 tx.execute(
231 "INSERT INTO messages (session_id, idx, role, content, tool_calls, tool_results, timestamp)
232 VALUES (?, ?, ?, ?, ?, ?, ?)",
233 params![
234 session.id,
235 idx as i64,
236 format!("{:?}", msg.role),
237 msg.content,
238 tool_calls_json,
239 tool_results_json,
240 msg.timestamp.timestamp(),
241 ],
242 )
243 .map_err(|e| SessionError::Storage(format!("Failed to insert message {}: {}", idx, e)))?;
244 }
245
246 tx.commit()
248 .map_err(|e| SessionError::Storage(format!("Failed to commit transaction: {}", e)))?;
249
250 Ok(())
251 }
252
253 async fn list_sessions(&self) -> Result<Vec<SessionSummary>, SessionError> {
254 let conn = self.conn.lock().unwrap();
255
256 let mut stmt = conn
257 .prepare(
258 "SELECT s.id, s.directory, s.created_at, s.updated_at, COUNT(m.id) as msg_count
259 FROM sessions s
260 LEFT JOIN messages m ON s.id = m.session_id
261 GROUP BY s.id
262 ORDER BY s.updated_at DESC",
263 )
264 .map_err(|e| SessionError::Storage(e.to_string()))?;
265
266 let sessions = stmt
267 .query_map(params![], |row| {
268 Ok((
269 row.get::<_, String>(0)?,
270 row.get::<_, String>(1)?,
271 row.get::<_, i64>(2)?,
272 row.get::<_, i64>(3)?,
273 row.get::<_, i64>(4)? as usize,
274 ))
275 })
276 .map_err(|e| SessionError::Storage(e.to_string()))?
277 .collect::<Result<Vec<_>, _>>()
278 .map_err(|e| SessionError::Storage(e.to_string()))?
279 .into_iter()
280 .map(
281 |(id, directory, created_at, updated_at, message_count)| SessionSummary {
282 id,
283 directory,
284 message_count,
285 created_at: DateTime::from_timestamp(created_at, 0).unwrap_or(Utc::now()),
286 updated_at: DateTime::from_timestamp(updated_at, 0).unwrap_or(Utc::now()),
287 },
288 )
289 .collect();
290
291 Ok(sessions)
292 }
293
294 async fn delete_session(&self, id: &str) -> Result<(), SessionError> {
295 let conn = self.conn.lock().unwrap();
296
297 let rows = conn
298 .execute("DELETE FROM sessions WHERE id = ?", params![id])
299 .map_err(|e| SessionError::Storage(e.to_string()))?;
300
301 if rows == 0 {
302 Err(SessionError::NotFound(id.to_string()))
303 } else {
304 Ok(())
305 }
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use tempfile::TempDir;
313
314 #[tokio::test]
315 async fn test_create_and_retrieve_session() {
316 let temp_dir = TempDir::new().unwrap();
317 let db_path = temp_dir.path().join("test.db");
318 let store = SqliteStore::new(db_path).unwrap();
319
320 let session = store.get_or_create_session().await.unwrap();
322 assert!(!session.id.is_empty());
323 assert_eq!(session.messages.len(), 0);
324
325 let retrieved = store.get_session(&session.id).await.unwrap().unwrap();
327 assert_eq!(retrieved.id, session.id);
328 }
329
330 #[tokio::test]
331 async fn test_save_and_load_session() {
332 let temp_dir = TempDir::new().unwrap();
333 let db_path = temp_dir.path().join("test.db");
334 let store = SqliteStore::new(db_path).unwrap();
335
336 let mut session = store.get_or_create_session().await.unwrap();
337
338 session.messages.push(SessionMessage {
340 role: MessageRole::User,
341 content: "Hello".to_string(),
342 tool_calls: vec![],
343 tool_results: vec![],
344 timestamp: Utc::now(),
345 });
346
347 store.save_session(&session).await.unwrap();
349
350 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
352 assert_eq!(loaded.messages.len(), 1);
353 assert_eq!(loaded.messages[0].content, "Hello");
354 assert_eq!(loaded.messages[0].role, MessageRole::User);
355 }
356
357 #[tokio::test]
358 async fn test_list_sessions() {
359 let temp_dir = TempDir::new().unwrap();
360 let db_path = temp_dir.path().join("test.db");
361 let store = SqliteStore::new(db_path).unwrap();
362
363 let session = store.get_or_create_session().await.unwrap();
365
366 let sessions = store.list_sessions().await.unwrap();
368 assert_eq!(sessions.len(), 1);
369 assert_eq!(sessions[0].id, session.id);
370 }
371
372 #[tokio::test]
373 async fn test_delete_session() {
374 let temp_dir = TempDir::new().unwrap();
375 let db_path = temp_dir.path().join("test.db");
376 let store = SqliteStore::new(db_path).unwrap();
377
378 let session = store.get_or_create_session().await.unwrap();
379
380 store.delete_session(&session.id).await.unwrap();
382
383 let retrieved = store.get_session(&session.id).await.unwrap();
385 assert!(retrieved.is_none());
386 }
387
388 #[tokio::test]
389 async fn test_delete_nonexistent_session() {
390 let temp_dir = TempDir::new().unwrap();
391 let db_path = temp_dir.path().join("test.db");
392 let store = SqliteStore::new(db_path).unwrap();
393
394 let result = store.delete_session("nonexistent-id").await;
395 assert!(result.is_err());
396 assert!(matches!(result, Err(SessionError::NotFound(_))));
397 }
398
399 #[tokio::test]
400 async fn test_save_nonexistent_session() {
401 let temp_dir = TempDir::new().unwrap();
402 let db_path = temp_dir.path().join("test.db");
403 let store = SqliteStore::new(db_path).unwrap();
404
405 let fake_session = Session {
407 id: uuid::Uuid::new_v4().to_string(),
408 created_at: Utc::now(),
409 updated_at: Utc::now(),
410 directory: "/fake/dir".to_string(),
411 messages: vec![],
412 };
413
414 let result = store.save_session(&fake_session).await;
416 assert!(result.is_err());
417 assert!(matches!(result, Err(SessionError::NotFound(_))));
418 }
419
420 #[tokio::test]
421 async fn test_large_session_with_many_messages() {
422 let temp_dir = TempDir::new().unwrap();
423 let db_path = temp_dir.path().join("test.db");
424 let store = SqliteStore::new(db_path).unwrap();
425
426 let mut session = store.get_or_create_session().await.unwrap();
427
428 for i in 0..100 {
430 session.messages.push(SessionMessage {
431 role: if i % 2 == 0 {
432 MessageRole::User
433 } else {
434 MessageRole::Assistant
435 },
436 content: format!("Message number {}", i),
437 tool_calls: vec![],
438 tool_results: vec![],
439 timestamp: Utc::now(),
440 });
441 }
442
443 store.save_session(&session).await.unwrap();
445
446 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
448 assert_eq!(loaded.messages.len(), 100);
449 assert_eq!(loaded.messages[0].content, "Message number 0");
450 assert_eq!(loaded.messages[99].content, "Message number 99");
451 }
452
453 #[tokio::test]
454 async fn test_session_with_tool_calls() {
455 let temp_dir = TempDir::new().unwrap();
456 let db_path = temp_dir.path().join("test.db");
457 let store = SqliteStore::new(db_path).unwrap();
458
459 let mut session = store.get_or_create_session().await.unwrap();
460
461 session.messages.push(SessionMessage {
463 role: MessageRole::Assistant,
464 content: "Let me call a tool".to_string(),
465 tool_calls: vec![ToolCall {
466 id: "call_1".to_string(),
467 name: "calculator".to_string(),
468 input: r#"{"operation": "add", "a": 5, "b": 3}"#.to_string(),
469 }],
470 tool_results: vec![ToolResult {
471 tool_use_id: "call_1".to_string(),
472 success: true,
473 content: "8".to_string(),
474 }],
475 timestamp: Utc::now(),
476 });
477
478 store.save_session(&session).await.unwrap();
480 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
481
482 assert_eq!(loaded.messages.len(), 1);
483 assert_eq!(loaded.messages[0].tool_calls.len(), 1);
484 assert_eq!(loaded.messages[0].tool_calls[0].name, "calculator");
485 assert_eq!(loaded.messages[0].tool_results.len(), 1);
486 assert_eq!(loaded.messages[0].tool_results[0].content, "8");
487 }
488
489 #[tokio::test]
490 async fn test_update_existing_session() {
491 let temp_dir = TempDir::new().unwrap();
492 let db_path = temp_dir.path().join("test.db");
493 let store = SqliteStore::new(db_path).unwrap();
494
495 let mut session = store.get_or_create_session().await.unwrap();
497 session.messages.push(SessionMessage {
498 role: MessageRole::User,
499 content: "First message".to_string(),
500 tool_calls: vec![],
501 tool_results: vec![],
502 timestamp: Utc::now(),
503 });
504 store.save_session(&session).await.unwrap();
505
506 session.messages.push(SessionMessage {
508 role: MessageRole::Assistant,
509 content: "Second message".to_string(),
510 tool_calls: vec![],
511 tool_results: vec![],
512 timestamp: Utc::now(),
513 });
514 store.save_session(&session).await.unwrap();
515
516 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
518 assert_eq!(loaded.messages.len(), 2);
519 assert_eq!(loaded.messages[0].content, "First message");
520 assert_eq!(loaded.messages[1].content, "Second message");
521 }
522
523 #[tokio::test]
524 async fn test_get_or_create_returns_existing() {
525 let temp_dir = TempDir::new().unwrap();
526 let db_path = temp_dir.path().join("test.db");
527 let store = SqliteStore::new(db_path).unwrap();
528
529 let session1 = store.get_or_create_session().await.unwrap();
531
532 let session2 = store.get_or_create_session().await.unwrap();
534
535 assert_eq!(session1.id, session2.id);
536
537 let sessions = store.list_sessions().await.unwrap();
539 assert_eq!(sessions.len(), 1);
540 }
541
542 #[tokio::test]
543 async fn test_default_location() {
544 let store = SqliteStore::default_location().unwrap();
545
546 let session = store.get_or_create_session().await.unwrap();
548 assert!(!session.id.is_empty());
549
550 std::fs::remove_dir_all(".mixtape").ok();
552 }
553
554 #[tokio::test]
555 async fn test_create_nested_directory() {
556 let temp_dir = TempDir::new().unwrap();
557 let db_path = temp_dir.path().join("deeply/nested/path/test.db");
558
559 let store = SqliteStore::new(&db_path).unwrap();
561 assert!(db_path.exists());
562
563 let session = store.get_or_create_session().await.unwrap();
565 assert!(!session.id.is_empty());
566 }
567
568 #[tokio::test]
569 async fn test_get_nonexistent_session() {
570 let temp_dir = TempDir::new().unwrap();
571 let db_path = temp_dir.path().join("test.db");
572 let store = SqliteStore::new(db_path).unwrap();
573
574 let result = store.get_session("nonexistent-id").await.unwrap();
576 assert!(result.is_none());
577 }
578
579 #[tokio::test]
580 async fn test_message_roles() {
581 let temp_dir = TempDir::new().unwrap();
582 let db_path = temp_dir.path().join("test.db");
583 let store = SqliteStore::new(db_path).unwrap();
584
585 let mut session = store.get_or_create_session().await.unwrap();
586
587 session.messages.push(SessionMessage {
589 role: MessageRole::User,
590 content: "User message".to_string(),
591 tool_calls: vec![],
592 tool_results: vec![],
593 timestamp: Utc::now(),
594 });
595 session.messages.push(SessionMessage {
596 role: MessageRole::Assistant,
597 content: "Assistant message".to_string(),
598 tool_calls: vec![],
599 tool_results: vec![],
600 timestamp: Utc::now(),
601 });
602 session.messages.push(SessionMessage {
603 role: MessageRole::System,
604 content: "System message".to_string(),
605 tool_calls: vec![],
606 tool_results: vec![],
607 timestamp: Utc::now(),
608 });
609
610 store.save_session(&session).await.unwrap();
611 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
612
613 assert_eq!(loaded.messages[0].role, MessageRole::User);
614 assert_eq!(loaded.messages[1].role, MessageRole::Assistant);
615 assert_eq!(loaded.messages[2].role, MessageRole::System);
616 }
617
618 #[tokio::test]
619 async fn test_session_summary_message_count() {
620 let temp_dir = TempDir::new().unwrap();
621 let db_path = temp_dir.path().join("test.db");
622 let store = SqliteStore::new(db_path).unwrap();
623
624 let mut session = store.get_or_create_session().await.unwrap();
625
626 for i in 0..5 {
628 session.messages.push(SessionMessage {
629 role: MessageRole::User,
630 content: format!("Message {}", i),
631 tool_calls: vec![],
632 tool_results: vec![],
633 timestamp: Utc::now(),
634 });
635 }
636 store.save_session(&session).await.unwrap();
637
638 let sessions = store.list_sessions().await.unwrap();
640 assert_eq!(sessions.len(), 1);
641 assert_eq!(sessions[0].message_count, 5);
642 }
643
644 #[tokio::test]
645 async fn test_session_with_multiple_tool_calls() {
646 let temp_dir = TempDir::new().unwrap();
647 let db_path = temp_dir.path().join("test.db");
648 let store = SqliteStore::new(db_path).unwrap();
649
650 let mut session = store.get_or_create_session().await.unwrap();
651
652 session.messages.push(SessionMessage {
654 role: MessageRole::Assistant,
655 content: "Using multiple tools".to_string(),
656 tool_calls: vec![
657 ToolCall {
658 id: "call_1".to_string(),
659 name: "search".to_string(),
660 input: r#"{"query": "hello"}"#.to_string(),
661 },
662 ToolCall {
663 id: "call_2".to_string(),
664 name: "read_file".to_string(),
665 input: r#"{"path": "/tmp/file.txt"}"#.to_string(),
666 },
667 ToolCall {
668 id: "call_3".to_string(),
669 name: "write_file".to_string(),
670 input: r#"{"path": "/tmp/out.txt", "content": "data"}"#.to_string(),
671 },
672 ],
673 tool_results: vec![
674 ToolResult {
675 tool_use_id: "call_1".to_string(),
676 success: true,
677 content: "Search results".to_string(),
678 },
679 ToolResult {
680 tool_use_id: "call_2".to_string(),
681 success: true,
682 content: "File content".to_string(),
683 },
684 ToolResult {
685 tool_use_id: "call_3".to_string(),
686 success: false,
687 content: "Permission denied".to_string(),
688 },
689 ],
690 timestamp: Utc::now(),
691 });
692
693 store.save_session(&session).await.unwrap();
694 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
695
696 assert_eq!(loaded.messages[0].tool_calls.len(), 3);
697 assert_eq!(loaded.messages[0].tool_results.len(), 3);
698 assert!(!loaded.messages[0].tool_results[2].success);
699 }
700
701 #[tokio::test]
702 async fn test_session_preserves_timestamps() {
703 let temp_dir = TempDir::new().unwrap();
704 let db_path = temp_dir.path().join("test.db");
705 let store = SqliteStore::new(db_path).unwrap();
706
707 let mut session = store.get_or_create_session().await.unwrap();
708
709 let specific_time = DateTime::from_timestamp(1700000000, 0).unwrap();
711 session.messages.push(SessionMessage {
712 role: MessageRole::User,
713 content: "Timed message".to_string(),
714 tool_calls: vec![],
715 tool_results: vec![],
716 timestamp: specific_time,
717 });
718
719 store.save_session(&session).await.unwrap();
720 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
721
722 assert_eq!(
724 loaded.messages[0].timestamp.timestamp(),
725 specific_time.timestamp()
726 );
727 }
728
729 #[tokio::test]
730 async fn test_empty_tool_calls_and_results() {
731 let temp_dir = TempDir::new().unwrap();
732 let db_path = temp_dir.path().join("test.db");
733 let store = SqliteStore::new(db_path).unwrap();
734
735 let mut session = store.get_or_create_session().await.unwrap();
736
737 session.messages.push(SessionMessage {
739 role: MessageRole::User,
740 content: "Regular message".to_string(),
741 tool_calls: vec![],
742 tool_results: vec![],
743 timestamp: Utc::now(),
744 });
745
746 store.save_session(&session).await.unwrap();
747 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
748
749 assert!(loaded.messages[0].tool_calls.is_empty());
750 assert!(loaded.messages[0].tool_results.is_empty());
751 }
752
753 #[tokio::test]
754 async fn test_session_directory_matches_current() {
755 let temp_dir = TempDir::new().unwrap();
756 let db_path = temp_dir.path().join("test.db");
757 let store = SqliteStore::new(db_path).unwrap();
758
759 let session = store.get_or_create_session().await.unwrap();
760
761 let current_dir = std::env::current_dir().unwrap().display().to_string();
763 assert_eq!(session.directory, current_dir);
764 }
765
766 #[tokio::test]
767 async fn test_list_empty_sessions() {
768 let temp_dir = TempDir::new().unwrap();
769 let db_path = temp_dir.path().join("test.db");
770 let store = SqliteStore::new(db_path).unwrap();
771
772 let sessions = store.list_sessions().await.unwrap();
774 assert!(sessions.is_empty());
775 }
776
777 #[tokio::test]
778 async fn test_unicode_content() {
779 let temp_dir = TempDir::new().unwrap();
780 let db_path = temp_dir.path().join("test.db");
781 let store = SqliteStore::new(db_path).unwrap();
782
783 let mut session = store.get_or_create_session().await.unwrap();
784
785 session.messages.push(SessionMessage {
787 role: MessageRole::User,
788 content: "Hello 世界! 🌍 Привет مرحبا".to_string(),
789 tool_calls: vec![ToolCall {
790 id: "unicode_call".to_string(),
791 name: "工具".to_string(),
792 input: r#"{"text": "日本語"}"#.to_string(),
793 }],
794 tool_results: vec![ToolResult {
795 tool_use_id: "unicode_call".to_string(),
796 success: true,
797 content: "Ελληνικά".to_string(),
798 }],
799 timestamp: Utc::now(),
800 });
801
802 store.save_session(&session).await.unwrap();
803 let loaded = store.get_session(&session.id).await.unwrap().unwrap();
804
805 assert_eq!(loaded.messages[0].content, "Hello 世界! 🌍 Привет مرحبا");
806 assert_eq!(loaded.messages[0].tool_calls[0].name, "工具");
807 assert_eq!(loaded.messages[0].tool_results[0].content, "Ελληνικά");
808 }
809}