use std::path::Path;
use crate::workspace::Workspace;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecoveryStatus {
Valid,
Recovered,
Unrecoverable(String),
}
#[derive(Debug, Clone)]
struct StateValidation {
pub is_valid: bool,
pub issues: Vec<String>,
}
fn validate_agent_state_with_workspace(
workspace: &dyn Workspace,
agent_dir: &Path,
) -> StateValidation {
let issues: Vec<String> = if !workspace.exists(agent_dir) {
vec![".agent/ directory does not exist".to_string()]
} else {
let unreadable_files: Vec<String> = workspace
.read_dir(agent_dir)
.ok()
.map(|entries| {
entries
.iter()
.filter_map(|entry| {
let path = entry.path();
if entry.is_file() && workspace.read(path).is_err() {
Some(format!("Corrupted file: {}", path.display()))
} else {
None
}
})
.collect()
})
.unwrap_or_default();
let zero_length_files: Vec<String> = [
"PLAN.md",
"ISSUES.md",
"STATUS.md",
"NOTES.md",
"commit-message.txt",
]
.iter()
.filter_map(|filename| {
let file_path = agent_dir.join(filename);
let content = workspace.read(&file_path).ok();
if content.is_some_and(|c| c.is_empty()) {
Some(format!("Zero-length file: {filename}"))
} else {
None
}
})
.collect();
unreadable_files
.into_iter()
.chain(zero_length_files)
.collect()
};
StateValidation {
is_valid: issues.is_empty(),
issues,
}
}
fn remove_zero_length_files_with_workspace(
workspace: &dyn Workspace,
agent_dir: &Path,
) -> std::io::Result<usize> {
let filenames = [
"PLAN.md",
"ISSUES.md",
"STATUS.md",
"NOTES.md",
"commit-message.txt",
];
let removed: usize = filenames
.iter()
.filter_map(|filename| {
let file_path = agent_dir.join(filename);
if !workspace.exists(&file_path) {
return None;
}
if let Ok(content) = workspace.read(&file_path) {
if content.is_empty() {
workspace.remove(&file_path).ok()?;
return Some(1);
}
}
None
})
.sum();
Ok(removed)
}
pub fn auto_repair_with_workspace(
workspace: &dyn Workspace,
agent_dir: &Path,
) -> std::io::Result<RecoveryStatus> {
if !workspace.exists(agent_dir) {
workspace.create_dir_all(&agent_dir.join("logs"))?;
return Ok(RecoveryStatus::Recovered);
}
let validation = validate_agent_state_with_workspace(workspace, agent_dir);
if validation.is_valid {
workspace.create_dir_all(&agent_dir.join("logs"))?;
return Ok(RecoveryStatus::Valid);
}
remove_zero_length_files_with_workspace(workspace, agent_dir)?;
workspace.create_dir_all(&agent_dir.join("logs"))?;
let post_validation = validate_agent_state_with_workspace(workspace, agent_dir);
if post_validation.is_valid {
Ok(RecoveryStatus::Recovered)
} else {
Ok(RecoveryStatus::Unrecoverable(format!(
"Unresolved .agent issues: {}",
post_validation.issues.join(", ")
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::workspace::MemoryWorkspace;
use std::path::Path;
#[test]
fn auto_repair_with_workspace_creates_missing_directory() {
let workspace = MemoryWorkspace::new_test();
let agent_dir = Path::new(".agent");
let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
assert_eq!(status, RecoveryStatus::Recovered);
assert!(workspace.exists(&agent_dir.join("logs")));
}
#[test]
fn auto_repair_with_workspace_removes_zero_length_files() {
let workspace = MemoryWorkspace::new_test()
.with_file(".agent/logs/.keep", "")
.with_file(".agent/PLAN.md", "");
let agent_dir = Path::new(".agent");
let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
assert_eq!(status, RecoveryStatus::Recovered);
assert!(!workspace.exists(&agent_dir.join("PLAN.md")));
}
#[test]
fn auto_repair_with_workspace_valid_state() {
let workspace = MemoryWorkspace::new_test()
.with_file(".agent/logs/.keep", "")
.with_file(".agent/PLAN.md", "# Plan\nSome content");
let agent_dir = Path::new(".agent");
let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
assert_eq!(status, RecoveryStatus::Valid);
assert!(workspace.exists(&agent_dir.join("PLAN.md")));
}
#[test]
fn auto_repair_with_workspace_multiple_zero_length_files() {
let workspace = MemoryWorkspace::new_test()
.with_file(".agent/logs/.keep", "")
.with_file(".agent/PLAN.md", "")
.with_file(".agent/ISSUES.md", "")
.with_file(".agent/STATUS.md", "valid content");
let agent_dir = Path::new(".agent");
let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
assert_eq!(status, RecoveryStatus::Recovered);
assert!(!workspace.exists(&agent_dir.join("PLAN.md")));
assert!(!workspace.exists(&agent_dir.join("ISSUES.md")));
assert!(workspace.exists(&agent_dir.join("STATUS.md")));
}
}