Skip to main content

retro_core/
runner.rs

1use crate::config::Config;
2use crate::db;
3use crate::errors::CoreError;
4use chrono::{DateTime, Utc};
5use rusqlite::Connection;
6use std::path::Path;
7
8const MAX_LOG_SIZE: u64 = 1_000_000; // 1 MB
9
10/// Rotate runner.log if it exceeds 1 MB.
11pub fn rotate_log_if_needed(retro_dir: &Path) -> Result<(), CoreError> {
12    let log_path = retro_dir.join("runner.log");
13    if !log_path.exists() {
14        return Ok(());
15    }
16    let metadata = std::fs::metadata(&log_path)
17        .map_err(|e| CoreError::Io(format!("reading runner.log metadata: {e}")))?;
18    if metadata.len() < MAX_LOG_SIZE {
19        return Ok(());
20    }
21    let backup_path = retro_dir.join("runner.log.1");
22    std::fs::rename(&log_path, &backup_path)
23        .map_err(|e| CoreError::Io(format!("rotating runner.log: {e}")))?;
24    std::fs::write(&log_path, "")
25        .map_err(|e| CoreError::Io(format!("creating fresh runner.log: {e}")))?;
26    Ok(())
27}
28
29/// Get the last run timestamp from the metadata table.
30pub fn last_run_time(conn: &Connection) -> Option<DateTime<Utc>> {
31    db::get_metadata(conn, "last_run_at")
32        .ok()
33        .flatten()
34        .and_then(|ts| {
35            DateTime::parse_from_rfc3339(&ts)
36                .ok()
37                .map(|dt| dt.with_timezone(&Utc))
38        })
39}
40
41/// Get AI calls used today and the configured max. Resets count on new day.
42pub fn ai_calls_today(conn: &Connection, config: &Config) -> (u32, u32) {
43    let today = Utc::now().format("%Y-%m-%d").to_string();
44    let date = db::get_metadata(conn, "ai_calls_date")
45        .ok()
46        .flatten()
47        .unwrap_or_default();
48    let count = db::get_metadata(conn, "ai_calls_today")
49        .ok()
50        .flatten()
51        .and_then(|s| s.parse::<u32>().ok())
52        .unwrap_or(0);
53    let used = if date == today { count } else { 0 };
54    (used, config.runner.max_ai_calls_per_day)
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use std::fs;
61    use tempfile::TempDir;
62
63    #[test]
64    fn test_rotate_log_skips_small_file() {
65        let dir = TempDir::new().unwrap();
66        let log_path = dir.path().join("runner.log");
67        fs::write(&log_path, "small content").unwrap();
68        rotate_log_if_needed(dir.path()).unwrap();
69        assert!(log_path.exists());
70        assert!(!dir.path().join("runner.log.1").exists());
71        assert_eq!(fs::read_to_string(&log_path).unwrap(), "small content");
72    }
73
74    #[test]
75    fn test_rotate_log_rotates_large_file() {
76        let dir = TempDir::new().unwrap();
77        let log_path = dir.path().join("runner.log");
78        let content = "x".repeat(1_100_000);
79        fs::write(&log_path, &content).unwrap();
80        rotate_log_if_needed(dir.path()).unwrap();
81        assert!(dir.path().join("runner.log.1").exists());
82        assert_eq!(
83            fs::read_to_string(dir.path().join("runner.log.1")).unwrap(),
84            content
85        );
86        assert!(log_path.exists());
87        assert_eq!(fs::read_to_string(&log_path).unwrap(), "");
88    }
89
90    #[test]
91    fn test_rotate_log_overwrites_old_backup() {
92        let dir = TempDir::new().unwrap();
93        let log_path = dir.path().join("runner.log");
94        let backup_path = dir.path().join("runner.log.1");
95        fs::write(&backup_path, "old backup").unwrap();
96        let content = "y".repeat(1_100_000);
97        fs::write(&log_path, &content).unwrap();
98        rotate_log_if_needed(dir.path()).unwrap();
99        assert_eq!(fs::read_to_string(&backup_path).unwrap(), content);
100    }
101
102    #[test]
103    fn test_rotate_log_no_file_ok() {
104        let dir = TempDir::new().unwrap();
105        rotate_log_if_needed(dir.path()).unwrap();
106    }
107
108    #[test]
109    fn test_last_run_time_none_when_not_set() {
110        let conn = Connection::open_in_memory().unwrap();
111        conn.pragma_update(None, "journal_mode", "WAL").unwrap();
112        db::init_db(&conn).unwrap();
113        assert!(last_run_time(&conn).is_none());
114    }
115
116    #[test]
117    fn test_last_run_time_returns_timestamp() {
118        let conn = Connection::open_in_memory().unwrap();
119        conn.pragma_update(None, "journal_mode", "WAL").unwrap();
120        db::init_db(&conn).unwrap();
121        let now = Utc::now();
122        db::set_metadata(&conn, "last_run_at", &now.to_rfc3339()).unwrap();
123        let result = last_run_time(&conn);
124        assert!(result.is_some());
125        assert!((result.unwrap() - now).num_seconds().abs() < 1);
126    }
127
128    #[test]
129    fn test_ai_calls_today_new_day_resets() {
130        let conn = Connection::open_in_memory().unwrap();
131        conn.pragma_update(None, "journal_mode", "WAL").unwrap();
132        db::init_db(&conn).unwrap();
133        let config = Config::default();
134        db::set_metadata(&conn, "ai_calls_date", "2020-01-01").unwrap();
135        db::set_metadata(&conn, "ai_calls_today", "5").unwrap();
136        let (used, max) = ai_calls_today(&conn, &config);
137        assert_eq!(used, 0);
138        assert_eq!(max, config.runner.max_ai_calls_per_day);
139    }
140
141    #[test]
142    fn test_ai_calls_today_same_day() {
143        let conn = Connection::open_in_memory().unwrap();
144        conn.pragma_update(None, "journal_mode", "WAL").unwrap();
145        db::init_db(&conn).unwrap();
146        let config = Config::default();
147        let today = Utc::now().format("%Y-%m-%d").to_string();
148        db::set_metadata(&conn, "ai_calls_date", &today).unwrap();
149        db::set_metadata(&conn, "ai_calls_today", "3").unwrap();
150        let (used, max) = ai_calls_today(&conn, &config);
151        assert_eq!(used, 3);
152        assert_eq!(max, config.runner.max_ai_calls_per_day);
153    }
154}