edda_transcript/
cursor.rs1use serde::{Deserialize, Serialize};
2use std::path::Path;
3
4#[derive(Debug, Serialize, Deserialize, Clone)]
5pub struct TranscriptCursor {
6 pub offset: u64,
7 pub file_size: u64,
8 pub mtime_unix: i64,
9 pub updated_at_unix: i64,
10}
11
12impl TranscriptCursor {
13 pub fn load(state_dir: &Path, session_id: &str) -> anyhow::Result<Option<Self>> {
14 let path = state_dir.join(format!("transcript_cursor.{session_id}.json"));
15 if !path.exists() {
16 return Ok(None);
17 }
18 let content = std::fs::read_to_string(&path)?;
19 let cursor: Self = serde_json::from_str(&content)?;
20 Ok(Some(cursor))
21 }
22
23 pub fn save(&self, state_dir: &Path, session_id: &str) -> anyhow::Result<()> {
24 let path = state_dir.join(format!("transcript_cursor.{session_id}.json"));
25 let data = serde_json::to_string_pretty(self)?;
26 edda_store::write_atomic(&path, data.as_bytes())
27 }
28
29 pub fn detect_truncation(&mut self, current_file_size: u64) {
31 if current_file_size < self.offset {
32 self.offset = 0;
33 }
34 }
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40
41 #[test]
42 fn cursor_save_and_load() {
43 let tmp = tempfile::tempdir().unwrap();
44 let cursor = TranscriptCursor {
45 offset: 100,
46 file_size: 5000,
47 mtime_unix: 1700000000,
48 updated_at_unix: 1700000001,
49 };
50 cursor.save(tmp.path(), "sess1").unwrap();
51 let loaded = TranscriptCursor::load(tmp.path(), "sess1")
52 .unwrap()
53 .unwrap();
54 assert_eq!(loaded.offset, 100);
55 assert_eq!(loaded.file_size, 5000);
56 }
57
58 #[test]
59 fn cursor_load_missing_returns_none() {
60 let tmp = tempfile::tempdir().unwrap();
61 let result = TranscriptCursor::load(tmp.path(), "missing").unwrap();
62 assert!(result.is_none());
63 }
64
65 #[test]
66 fn truncation_detection() {
67 let mut cursor = TranscriptCursor {
68 offset: 5000,
69 file_size: 5000,
70 mtime_unix: 0,
71 updated_at_unix: 0,
72 };
73 cursor.detect_truncation(3000);
74 assert_eq!(cursor.offset, 0);
75 }
76}