use std::path::Path;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const EXECUTION_STATE_FILENAME: &str = "execution-state.json";
pub const CURRENT_EXECUTION_STATE_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionState {
#[serde(default)]
pub version: u32,
pub status: ExecutionStatus,
pub active_tasks: Vec<String>,
pub pending_approvals: Vec<ApprovalRequest>,
pub cancel_requested: bool,
pub last_updated: DateTime<Utc>,
}
impl Default for ExecutionState {
fn default() -> Self {
Self {
version: CURRENT_EXECUTION_STATE_VERSION,
status: ExecutionStatus::Idle,
active_tasks: Vec::new(),
pending_approvals: Vec::new(),
cancel_requested: false,
last_updated: Utc::now(),
}
}
}
impl ExecutionState {
pub fn new(status: ExecutionStatus) -> Self {
Self {
version: CURRENT_EXECUTION_STATE_VERSION,
status,
active_tasks: Vec::new(),
pending_approvals: Vec::new(),
cancel_requested: false,
last_updated: Utc::now(),
}
}
pub fn load(gid_dir: &Path) -> Result<Self> {
let path = gid_dir.join(EXECUTION_STATE_FILENAME);
if !path.exists() {
return Ok(Self::default());
}
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read {}", path.display()))?;
serde_json::from_str(&content)
.with_context(|| format!("Failed to parse {}", path.display()))
}
pub fn save(&mut self, gid_dir: &Path) -> Result<()> {
self.last_updated = Utc::now();
let path = gid_dir.join(EXECUTION_STATE_FILENAME);
std::fs::create_dir_all(gid_dir)
.with_context(|| format!("Failed to create {}", gid_dir.display()))?;
let content = serde_json::to_string_pretty(self)
.context("Failed to serialize execution state")?;
std::fs::write(&path, content)
.with_context(|| format!("Failed to write {}", path.display()))
}
pub fn start_running(&mut self) {
self.status = ExecutionStatus::Running;
self.cancel_requested = false;
}
pub fn set_active_tasks(&mut self, tasks: Vec<String>) {
self.active_tasks = tasks;
}
pub fn add_approval_request(&mut self, layer_index: usize, message: String) {
self.pending_approvals.push(ApprovalRequest {
layer_index,
message,
requested_at: Utc::now(),
});
self.status = ExecutionStatus::WaitingApproval;
}
pub fn approve(&mut self) -> Vec<ApprovalRequest> {
let approved = std::mem::take(&mut self.pending_approvals);
if !approved.is_empty() {
self.status = ExecutionStatus::Approved;
}
approved
}
pub fn request_cancel(&mut self) {
self.cancel_requested = true;
}
pub fn is_cancel_requested(&self) -> bool {
self.cancel_requested
}
pub fn complete(&mut self) {
self.status = ExecutionStatus::Completed;
self.active_tasks.clear();
self.pending_approvals.clear();
}
pub fn mark_cancelled(&mut self) {
self.status = ExecutionStatus::Cancelled;
self.active_tasks.clear();
}
pub fn pause(&mut self) {
self.status = ExecutionStatus::Paused;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionStatus {
Idle,
Running,
WaitingApproval,
Approved,
Paused,
Completed,
Cancelled,
}
impl std::fmt::Display for ExecutionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExecutionStatus::Idle => write!(f, "idle"),
ExecutionStatus::Running => write!(f, "running"),
ExecutionStatus::WaitingApproval => write!(f, "waiting_approval"),
ExecutionStatus::Approved => write!(f, "approved"),
ExecutionStatus::Paused => write!(f, "paused"),
ExecutionStatus::Completed => write!(f, "completed"),
ExecutionStatus::Cancelled => write!(f, "cancelled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalRequest {
pub layer_index: usize,
pub message: String,
pub requested_at: DateTime<Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_default_state() {
let state = ExecutionState::default();
assert_eq!(state.status, ExecutionStatus::Idle);
assert!(state.active_tasks.is_empty());
assert!(state.pending_approvals.is_empty());
assert!(!state.cancel_requested);
}
#[test]
fn test_load_nonexistent() {
let tmp = tempdir().unwrap();
let state = ExecutionState::load(tmp.path()).unwrap();
assert_eq!(state.status, ExecutionStatus::Idle);
}
#[test]
fn test_save_and_load() {
let tmp = tempdir().unwrap();
let gid_dir = tmp.path().join(".gid");
let mut state = ExecutionState::new(ExecutionStatus::Running);
state.active_tasks = vec!["task-1".to_string(), "task-2".to_string()];
state.save(&gid_dir).unwrap();
let loaded = ExecutionState::load(&gid_dir).unwrap();
assert_eq!(loaded.status, ExecutionStatus::Running);
assert_eq!(loaded.active_tasks, vec!["task-1", "task-2"]);
}
#[test]
fn test_approval_workflow() {
let mut state = ExecutionState::new(ExecutionStatus::Running);
state.add_approval_request(1, "Review layer 1 results".to_string());
assert_eq!(state.status, ExecutionStatus::WaitingApproval);
assert_eq!(state.pending_approvals.len(), 1);
let approved = state.approve();
assert_eq!(approved.len(), 1);
assert_eq!(approved[0].layer_index, 1);
assert_eq!(state.status, ExecutionStatus::Approved);
assert!(state.pending_approvals.is_empty());
}
#[test]
fn test_cancel_workflow() {
let mut state = ExecutionState::new(ExecutionStatus::Running);
state.active_tasks = vec!["task-1".to_string()];
assert!(!state.is_cancel_requested());
state.request_cancel();
assert!(state.is_cancel_requested());
state.mark_cancelled();
assert_eq!(state.status, ExecutionStatus::Cancelled);
assert!(state.active_tasks.is_empty());
}
#[test]
fn test_complete_workflow() {
let mut state = ExecutionState::new(ExecutionStatus::Running);
state.active_tasks = vec!["task-1".to_string()];
state.add_approval_request(0, "test".to_string());
state.complete();
assert_eq!(state.status, ExecutionStatus::Completed);
assert!(state.active_tasks.is_empty());
assert!(state.pending_approvals.is_empty());
}
#[test]
fn test_version_set_on_new() {
let state = ExecutionState::new(ExecutionStatus::Running);
assert_eq!(state.version, CURRENT_EXECUTION_STATE_VERSION);
assert_eq!(state.version, 1);
}
#[test]
fn test_version_set_on_default() {
let state = ExecutionState::default();
assert_eq!(state.version, CURRENT_EXECUTION_STATE_VERSION);
}
#[test]
fn test_version_serialized() {
let state = ExecutionState::new(ExecutionStatus::Idle);
let json = serde_json::to_string(&state).unwrap();
assert!(json.contains("\"version\":1"));
}
#[test]
fn test_backward_compat_no_version_field() {
let old_json = r#"{
"status": "running",
"active_tasks": ["task-1"],
"pending_approvals": [],
"cancel_requested": false,
"last_updated": "2026-04-07T10:00:00Z"
}"#;
let state: ExecutionState = serde_json::from_str(old_json).unwrap();
assert_eq!(state.version, 0);
assert_eq!(state.status, ExecutionStatus::Running);
assert_eq!(state.active_tasks, vec!["task-1"]);
}
#[test]
fn test_version_persists_through_save_load() {
let tmp = tempdir().unwrap();
let gid_dir = tmp.path().join(".gid");
let mut state = ExecutionState::new(ExecutionStatus::Running);
assert_eq!(state.version, 1);
state.save(&gid_dir).unwrap();
let loaded = ExecutionState::load(&gid_dir).unwrap();
assert_eq!(loaded.version, 1);
}
#[test]
fn test_status_display() {
assert_eq!(ExecutionStatus::Idle.to_string(), "idle");
assert_eq!(ExecutionStatus::Running.to_string(), "running");
assert_eq!(ExecutionStatus::WaitingApproval.to_string(), "waiting_approval");
assert_eq!(ExecutionStatus::Approved.to_string(), "approved");
assert_eq!(ExecutionStatus::Cancelled.to_string(), "cancelled");
}
}