malvin 0.2.6

Non-interactive research and coding agent
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};

pub const MODELS_REFRESH_INTERVAL_SECS: u64 = 24 * 60 * 60;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModelsRefreshRecord {
    #[serde(alias = "timestamp")]
    pub last_refresh_secs: u64,
}

#[must_use]
pub fn unix_now_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

#[must_use]
pub fn models_refresh_record_path() -> PathBuf {
    crate::workspace_paths::malvin_user_home_root().join("last_models_refresh.json")
}

#[must_use]
pub fn load_last_refresh_secs() -> Option<u64> {
    let path = models_refresh_record_path();
    let body = std::fs::read_to_string(path).ok()?;
    let record: ModelsRefreshRecord = serde_json::from_str(&body).ok()?;
    Some(record.last_refresh_secs)
}

pub fn save_last_refresh_secs(now_secs: u64) -> Result<(), String> {
    let path = models_refresh_record_path();
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    let record = ModelsRefreshRecord {
        last_refresh_secs: now_secs,
    };
    let json = serde_json::to_string_pretty(&record).map_err(|e| e.to_string())?;
    std::fs::write(&path, json).map_err(|e| format!("write {}: {e}", path.display()))
}

#[must_use]
pub fn models_refresh_is_due(now_secs: u64) -> bool {
    load_last_refresh_secs()
        .is_none_or(|last| now_secs.saturating_sub(last) >= MODELS_REFRESH_INTERVAL_SECS)
}

pub fn perform_models_refresh() {
    let now = unix_now_secs();
    let _ = crate::npm_pi_sdk::refresh_npm_pi_models();
    let _ = crate::pi_sdk::refresh_pi_provider_caches_if_stale(true);
    let _ = save_last_refresh_secs(now);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_record_with_alias_timestamp() {
        let json = r#"{"timestamp": 123456789}"#;
        let record: ModelsRefreshRecord = serde_json::from_str(json).expect("parse");
        assert_eq!(record.last_refresh_secs, 123_456_789);
    }

    #[test]
    fn parses_record_with_last_refresh_secs() {
        let json = r#"{"last_refresh_secs": 987654321}"#;
        let record: ModelsRefreshRecord = serde_json::from_str(json).expect("parse");
        assert_eq!(record.last_refresh_secs, 987_654_321);
    }

    #[test]
    fn unix_now_secs_is_reasonable() {
        assert!(unix_now_secs() > 1_700_000_000);
    }

    #[test]
    fn models_refresh_due_check_logic() {
        let now = 1_000_000_u64;
        let recent = now - 3600;
        assert!(now.saturating_sub(recent) < MODELS_REFRESH_INTERVAL_SECS);
        let stale = now - (MODELS_REFRESH_INTERVAL_SECS + 10);
        assert!(now.saturating_sub(stale) >= MODELS_REFRESH_INTERVAL_SECS);
    }
}