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; pub 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
29pub 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
41pub 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}