use std::path::PathBuf;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatchupState {
pub last_catchup_at: DateTime<Utc>,
pub palace_id: String,
pub last_git_sha: Option<String>,
}
fn catchup_dir(palace_id: &str) -> Option<PathBuf> {
let home = dirs::home_dir()?;
Some(home.join(".trusty-mpm").join("projects").join(palace_id))
}
fn state_path(palace_id: &str) -> Option<PathBuf> {
Some(catchup_dir(palace_id)?.join("catchup-state.json"))
}
pub fn load_catchup_state(palace_id: &str) -> Option<CatchupState> {
let path = state_path(palace_id)?;
let bytes = std::fs::read(&path).ok()?;
serde_json::from_slice(&bytes).ok()
}
pub fn save_catchup_state(palace_id: &str, state: &CatchupState) -> anyhow::Result<()> {
let dir = catchup_dir(palace_id)
.ok_or_else(|| anyhow::anyhow!("could not resolve home directory for catchup state"))?;
std::fs::create_dir_all(&dir)?;
let path = dir.join("catchup-state.json");
let json = serde_json::to_vec_pretty(state)?;
std::fs::write(&path, json)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write_state_to(dir: &TempDir, palace_id: &str, state: &CatchupState) {
let p = dir.path().join(palace_id);
fs::create_dir_all(&p).unwrap();
let json = serde_json::to_vec_pretty(state).unwrap();
fs::write(p.join("catchup-state.json"), json).unwrap();
}
fn load_state_from(dir: &TempDir, palace_id: &str) -> Option<CatchupState> {
let path = dir.path().join(palace_id).join("catchup-state.json");
let bytes = std::fs::read(&path).ok()?;
serde_json::from_slice(&bytes).ok()
}
#[test]
fn state_save_load_roundtrip() {
let tmp = TempDir::new().unwrap();
let state = CatchupState {
last_catchup_at: "2026-06-27T10:00:00Z".parse::<DateTime<Utc>>().unwrap(),
palace_id: "test-palace".to_string(),
last_git_sha: Some("abc1234".to_string()),
};
write_state_to(&tmp, "test-palace", &state);
let loaded = load_state_from(&tmp, "test-palace");
assert!(loaded.is_some(), "state should load back");
let loaded = loaded.unwrap();
assert_eq!(loaded.palace_id, "test-palace");
assert_eq!(loaded.last_git_sha.as_deref(), Some("abc1234"));
}
#[test]
fn state_missing_file_returns_none() {
let path = PathBuf::from("/tmp/nonexistent-palace-xyz-catchup/catchup-state.json");
let bytes = std::fs::read(&path);
assert!(bytes.is_err(), "file should not exist");
let result: Option<CatchupState> = bytes.ok().and_then(|b| serde_json::from_slice(&b).ok());
assert!(result.is_none());
}
#[test]
fn state_parse_failure_returns_none() {
let tmp = TempDir::new().unwrap();
let p = tmp.path().join("test-palace");
std::fs::create_dir_all(&p).unwrap();
std::fs::write(p.join("catchup-state.json"), b"not valid json {{").unwrap();
let bytes = std::fs::read(p.join("catchup-state.json")).ok();
let result: Option<CatchupState> = bytes.and_then(|b| serde_json::from_slice(&b).ok());
assert!(result.is_none(), "invalid JSON should yield None");
}
}