mixtape_cli/session/
store.rs

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
11/// SQLite-based session storage
12///
13/// Sessions are stored in a local SQLite database, scoped to the
14/// current working directory.
15///
16/// # Example
17/// ```no_run
18/// use mixtape_cli::SqliteStore;
19/// use mixtape_core::Agent;
20///
21/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
22/// let store = SqliteStore::new(".mixtape/sessions.db")?;
23/// // Use with agent
24/// # Ok(())
25/// # }
26/// ```
27pub struct SqliteStore {
28    conn: Arc<Mutex<Connection>>,
29}
30
31impl SqliteStore {
32    /// Create new SQLite store at path
33    ///
34    /// Creates database file and tables if they don't exist.
35    /// Path can be relative or absolute.
36    pub fn new(path: impl Into<PathBuf>) -> Result<Self, SessionError> {
37        let path = path.into();
38
39        // Create parent directory if needed
40        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        // Initialize schema
49        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    /// Use default location (.mixtape/sessions.db in current directory)
58    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            // Try to find existing session for this directory
75            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            // Load existing session
86            self.get_session(&id)
87                .await?
88                .ok_or_else(|| SessionError::NotFound(id.clone()))
89        } else {
90            // Create new session
91            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        // Get session metadata
117        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        // Get messages
138        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        // Use a transaction for atomic save operation
197        // This ensures all-or-nothing: either everything saves or nothing does
198        let tx = conn
199            .transaction()
200            .map_err(|e| SessionError::Storage(format!("Failed to begin transaction: {}", e)))?;
201
202        // Update session timestamp
203        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 session doesn't exist, fail
212        if rows == 0 {
213            return Err(SessionError::NotFound(session.id.clone()));
214        }
215
216        // Delete old messages
217        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        // Insert new messages
224        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        // Commit transaction - if this fails, all changes are rolled back
247        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        // Create a session
321        let session = store.get_or_create_session().await.unwrap();
322        assert!(!session.id.is_empty());
323        assert_eq!(session.messages.len(), 0);
324
325        // Retrieve same session
326        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        // Add a message
339        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        // Save it
348        store.save_session(&session).await.unwrap();
349
350        // Load it back
351        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        // Create a session
364        let session = store.get_or_create_session().await.unwrap();
365
366        // List sessions
367        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        // Delete it
381        store.delete_session(&session.id).await.unwrap();
382
383        // Should not find it
384        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        // Create a fake session that doesn't exist in the DB
406        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        // Should fail because session doesn't exist
415        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        // Add 100 messages
429        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        // Save it
444        store.save_session(&session).await.unwrap();
445
446        // Load it back
447        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        // Add message with tool calls and results
462        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        // Save and reload
479        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        // Create session with one message
496        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        // Add another message
507        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        // Load and verify both messages
517        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        // Create first session
530        let session1 = store.get_or_create_session().await.unwrap();
531
532        // Get same session again (should not create new)
533        let session2 = store.get_or_create_session().await.unwrap();
534
535        assert_eq!(session1.id, session2.id);
536
537        // Verify only one session exists
538        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        // Should be able to create a session
547        let session = store.get_or_create_session().await.unwrap();
548        assert!(!session.id.is_empty());
549
550        // Cleanup
551        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        // Should create all parent directories
560        let store = SqliteStore::new(&db_path).unwrap();
561        assert!(db_path.exists());
562
563        // Should work normally
564        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        // Query for a session that doesn't exist
575        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        // Test all message role types
588        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        // Add 5 messages
627        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        // List sessions should show correct message count
639        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        // Add message with multiple tool calls
653        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        // Use a specific timestamp
710        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        // Timestamp should be preserved (at second precision)
723        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        // Message with empty tool arrays
738        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        // Directory should match current working directory
762        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        // List sessions on empty store
773        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        // Test unicode content
786        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}