use anyhow::Result;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CheckpointStorage {
Local(PathBuf),
Global {
repo_name: String,
},
Session {
session_id: String,
},
UnifiedSession {
session_id: String,
},
}
impl CheckpointStorage {
pub fn resolve_base_dir(&self) -> Result<PathBuf> {
match self {
Self::Local(path) => Ok(path.clone()),
Self::Global { repo_name } => {
let global_base = resolve_global_base_dir()?;
Ok(global_base
.join("state")
.join(repo_name)
.join("checkpoints"))
}
Self::Session { session_id } => {
let global_base = resolve_global_base_dir()?;
Ok(global_base
.join("state")
.join(session_id)
.join("checkpoints"))
}
Self::UnifiedSession { session_id } => {
let global_base = resolve_global_base_dir()?;
Ok(global_base.join("sessions").join(session_id))
}
}
}
pub fn checkpoint_file_path(&self, checkpoint_id: &str) -> Result<PathBuf> {
let base = self.resolve_base_dir()?;
match self {
Self::UnifiedSession { .. } => {
Ok(base.join("checkpoint.json"))
}
_ => {
Ok(base.join(format!("{}.checkpoint.json", checkpoint_id)))
}
}
}
}
pub fn resolve_global_base_dir() -> Result<PathBuf> {
crate::storage::get_default_storage_dir()
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn test_local_storage_uses_provided_path() {
let custom_path = PathBuf::from("/tmp/checkpoints");
let storage = CheckpointStorage::Local(custom_path.clone());
let base = storage.resolve_base_dir().unwrap();
assert_eq!(base, custom_path);
}
#[test]
fn test_session_storage_path_resolution() {
let storage = CheckpointStorage::Session {
session_id: "test-session-123".to_string(),
};
let base = storage.resolve_base_dir().unwrap();
assert!(base
.to_string_lossy()
.ends_with("state/test-session-123/checkpoints"));
let file = storage.checkpoint_file_path("checkpoint-1").unwrap();
assert!(file
.to_string_lossy()
.ends_with("checkpoint-1.checkpoint.json"));
}
#[test]
fn test_global_storage_path_resolution() {
let storage = CheckpointStorage::Global {
repo_name: "my-repo".to_string(),
};
let base = storage.resolve_base_dir().unwrap();
assert!(base
.to_string_lossy()
.ends_with("state/my-repo/checkpoints"));
}
#[test]
fn test_unified_session_storage_path_resolution() {
let storage = CheckpointStorage::UnifiedSession {
session_id: "test-session-456".to_string(),
};
let base = storage.resolve_base_dir().unwrap();
assert!(base
.to_string_lossy()
.ends_with("sessions/test-session-456"));
let file = storage.checkpoint_file_path("ignored-id").unwrap();
assert!(file.to_string_lossy().ends_with("/checkpoint.json"));
assert!(!file.to_string_lossy().contains("ignored-id"));
}
#[test]
fn test_unified_session_always_uses_checkpoint_json() {
let storage = CheckpointStorage::UnifiedSession {
session_id: "test-session".to_string(),
};
let path1 = storage.checkpoint_file_path("checkpoint-1").unwrap();
let path2 = storage.checkpoint_file_path("checkpoint-2").unwrap();
let path3 = storage.checkpoint_file_path("any-id").unwrap();
assert_eq!(path1, path2);
assert_eq!(path2, path3);
assert!(path1.to_string_lossy().ends_with("/checkpoint.json"));
}
#[test]
fn test_path_resolution_is_deterministic() {
let storage = CheckpointStorage::Session {
session_id: "session-abc".to_string(),
};
let path1 = storage.checkpoint_file_path("test").unwrap();
let path2 = storage.checkpoint_file_path("test").unwrap();
assert_eq!(path1, path2, "Same inputs must produce same path");
}
#[test]
fn test_checkpoint_file_path_includes_checkpoint_id() {
let storage = CheckpointStorage::Session {
session_id: "test-session".to_string(),
};
let path = storage.checkpoint_file_path("my-checkpoint").unwrap();
assert!(path
.file_name()
.unwrap()
.to_string_lossy()
.contains("my-checkpoint"));
assert!(path.to_string_lossy().ends_with(".checkpoint.json"));
}
#[test]
fn test_different_session_ids_produce_different_paths() {
let storage1 = CheckpointStorage::Session {
session_id: "session-1".to_string(),
};
let storage2 = CheckpointStorage::Session {
session_id: "session-2".to_string(),
};
let path1 = storage1.checkpoint_file_path("test").unwrap();
let path2 = storage2.checkpoint_file_path("test").unwrap();
assert_ne!(
path1, path2,
"Different session IDs must produce different paths"
);
}
#[test]
fn test_global_base_dir_resolution() {
let base = resolve_global_base_dir().unwrap();
assert!(!base.as_os_str().is_empty());
}
#[test]
fn test_storage_equality() {
let storage1 = CheckpointStorage::Session {
session_id: "test".to_string(),
};
let storage2 = CheckpointStorage::Session {
session_id: "test".to_string(),
};
let storage3 = CheckpointStorage::Session {
session_id: "different".to_string(),
};
assert_eq!(storage1, storage2);
assert_ne!(storage1, storage3);
}
proptest! {
#[test]
fn prop_same_strategy_same_path(
session_id in "[a-zA-Z0-9-]{1,50}",
checkpoint_id in "[a-zA-Z0-9-]{1,50}"
) {
let storage = CheckpointStorage::Session {
session_id: session_id.clone(),
};
let path1 = storage.checkpoint_file_path(&checkpoint_id).unwrap();
let path2 = storage.checkpoint_file_path(&checkpoint_id).unwrap();
prop_assert_eq!(path1, path2, "Same inputs must always produce same path");
}
#[test]
fn prop_different_sessions_different_paths(
session_id1 in "[a-zA-Z0-9-]{1,50}",
session_id2 in "[a-zA-Z0-9-]{1,50}",
checkpoint_id in "[a-zA-Z0-9-]{1,50}"
) {
prop_assume!(session_id1 != session_id2);
let storage1 = CheckpointStorage::Session {
session_id: session_id1,
};
let storage2 = CheckpointStorage::Session {
session_id: session_id2,
};
let path1 = storage1.checkpoint_file_path(&checkpoint_id).unwrap();
let path2 = storage2.checkpoint_file_path(&checkpoint_id).unwrap();
prop_assert_ne!(path1, path2, "Different session IDs must produce different paths");
}
#[test]
fn prop_checkpoint_paths_have_correct_extension(
session_id in "[a-zA-Z0-9-]{1,50}",
checkpoint_id in "[a-zA-Z0-9-]{1,50}"
) {
let storage = CheckpointStorage::Session { session_id };
let path = storage.checkpoint_file_path(&checkpoint_id).unwrap();
prop_assert!(
path.to_string_lossy().ends_with(".checkpoint.json"),
"Checkpoint paths must end with .checkpoint.json"
);
}
#[test]
fn prop_checkpoint_id_in_path(
session_id in "[a-zA-Z0-9-]{1,50}",
checkpoint_id in "[a-zA-Z0-9-]{1,50}"
) {
let storage = CheckpointStorage::Session { session_id };
let path = storage.checkpoint_file_path(&checkpoint_id).unwrap();
prop_assert!(
path.file_name()
.unwrap()
.to_string_lossy()
.contains(&checkpoint_id),
"Checkpoint ID must be in the file name"
);
}
#[test]
fn prop_global_storage_contains_repo_name(
repo_name in "[a-zA-Z0-9-]{1,50}"
) {
let storage = CheckpointStorage::Global {
repo_name: repo_name.clone(),
};
let base = storage.resolve_base_dir().unwrap();
prop_assert!(
base.to_string_lossy().contains(&repo_name),
"Global storage paths must contain repo name"
);
}
#[test]
fn prop_session_storage_contains_session_id(
session_id in "[a-zA-Z0-9-]{1,50}"
) {
let storage = CheckpointStorage::Session {
session_id: session_id.clone(),
};
let base = storage.resolve_base_dir().unwrap();
prop_assert!(
base.to_string_lossy().contains(&session_id),
"Session storage paths must contain session ID"
);
}
#[test]
fn prop_local_storage_returns_exact_path(
path_str in "[a-zA-Z0-9/_-]{1,100}"
) {
let expected_path = PathBuf::from(path_str);
let storage = CheckpointStorage::Local(expected_path.clone());
let resolved = storage.resolve_base_dir().unwrap();
prop_assert_eq!(resolved, expected_path, "Local storage must return exact path");
}
}
}