Skip to main content

toolpath_gemini/
reader.rs

1//! Read and parse Gemini CLI chat files and project logs.
2
3use crate::error::{ConvoError, Result};
4use crate::types::{ChatFile, LogEntry};
5use std::fs;
6use std::path::Path;
7
8pub struct ConversationReader;
9
10impl ConversationReader {
11    /// Parse a single chat JSON file.
12    pub fn read_chat_file<P: AsRef<Path>>(path: P) -> Result<ChatFile> {
13        let path = path.as_ref();
14        if !path.exists() {
15            return Err(ConvoError::ConversationNotFound(path.display().to_string()));
16        }
17        let bytes = fs::read(path)?;
18        let chat: ChatFile = serde_json::from_slice(&bytes)
19            .map_err(|e| ConvoError::Other(anyhow::anyhow!("{}: {}", path.display(), e)))?;
20        Ok(chat)
21    }
22
23    /// Return the byte-length of a chat file on disk.
24    pub fn file_size<P: AsRef<Path>>(path: P) -> Result<u64> {
25        let path = path.as_ref();
26        if !path.exists() {
27            return Err(ConvoError::ConversationNotFound(path.display().to_string()));
28        }
29        Ok(fs::metadata(path)?.len())
30    }
31
32    /// Parse `logs.json` — a JSON array of lightweight log entries.
33    /// Returns an empty vec when the file is absent or malformed (the log
34    /// is auxiliary; callers should not fail the whole operation when it's
35    /// missing).
36    pub fn read_logs<P: AsRef<Path>>(path: P) -> Result<Vec<LogEntry>> {
37        let path = path.as_ref();
38        if !path.exists() {
39            return Ok(Vec::new());
40        }
41        let bytes = fs::read(path)?;
42        match serde_json::from_slice::<Vec<LogEntry>>(&bytes) {
43            Ok(v) => Ok(v),
44            Err(e) => {
45                eprintln!(
46                    "Warning: Failed to parse Gemini log file {}: {}",
47                    path.display(),
48                    e
49                );
50                Ok(Vec::new())
51            }
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use std::io::Write;
60    use tempfile::NamedTempFile;
61
62    fn write_chat(body: &str) -> NamedTempFile {
63        let mut f = NamedTempFile::new().unwrap();
64        f.write_all(body.as_bytes()).unwrap();
65        f.flush().unwrap();
66        f
67    }
68
69    #[test]
70    fn test_read_chat_file() {
71        let f = write_chat(
72            r#"{"sessionId":"s","projectHash":"h","messages":[{"id":"m","timestamp":"t","type":"user","content":"hi"}]}"#,
73        );
74        let chat = ConversationReader::read_chat_file(f.path()).unwrap();
75        assert_eq!(chat.session_id, "s");
76        assert_eq!(chat.messages.len(), 1);
77    }
78
79    #[test]
80    fn test_read_chat_file_nonexistent() {
81        let err = ConversationReader::read_chat_file("/nonexistent.json").unwrap_err();
82        matches!(err, ConvoError::ConversationNotFound(_));
83    }
84
85    #[test]
86    fn test_read_chat_file_invalid_json() {
87        let f = write_chat("not json");
88        let err = ConversationReader::read_chat_file(f.path()).unwrap_err();
89        matches!(err, ConvoError::Other(_));
90    }
91
92    #[test]
93    fn test_read_logs() {
94        let f = write_chat(
95            r#"[{"sessionId":"s","messageId":0,"type":"user","message":"hi","timestamp":"t"}]"#,
96        );
97        let logs = ConversationReader::read_logs(f.path()).unwrap();
98        assert_eq!(logs.len(), 1);
99        assert_eq!(logs[0].message, "hi");
100    }
101
102    #[test]
103    fn test_read_logs_absent() {
104        let logs = ConversationReader::read_logs("/nonexistent.json").unwrap();
105        assert!(logs.is_empty());
106    }
107
108    #[test]
109    fn test_read_logs_malformed_returns_empty() {
110        let f = write_chat("{");
111        let logs = ConversationReader::read_logs(f.path()).unwrap();
112        assert!(logs.is_empty());
113    }
114
115    #[test]
116    fn test_file_size() {
117        let f = write_chat(r#"{"messages":[]}"#);
118        let size = ConversationReader::file_size(f.path()).unwrap();
119        assert!(size > 0);
120    }
121
122    #[test]
123    fn test_file_size_nonexistent() {
124        let err = ConversationReader::file_size("/nonexistent.json").unwrap_err();
125        matches!(err, ConvoError::ConversationNotFound(_));
126    }
127}