use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum StepStatus {
Pending,
InProgress,
Completed,
Failed(String),
Skipped(String),
}
impl StepStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
StepStatus::Completed | StepStatus::Failed(_) | StepStatus::Skipped(_)
)
}
pub fn is_success(&self) -> bool {
matches!(self, StepStatus::Completed)
}
}
impl fmt::Display for StepStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StepStatus::Pending => write!(f, "Pending"),
StepStatus::InProgress => write!(f, "InProgress"),
StepStatus::Completed => write!(f, "Completed"),
StepStatus::Failed(reason) => write!(f, "Failed: {}", reason),
StepStatus::Skipped(reason) => write!(f, "Skipped: {}", reason),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanStep {
pub id: String,
pub description: String,
pub status: StepStatus,
pub dependencies: Vec<String>,
pub estimated_tokens: Option<usize>,
pub result: Option<Value>,
pub metadata: HashMap<String, Value>,
}
impl PlanStep {
pub fn new(description: impl Into<String>) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
description: description.into(),
status: StepStatus::Pending,
dependencies: Vec::new(),
estimated_tokens: None,
result: None,
metadata: HashMap::new(),
}
}
pub fn with_dependency(mut self, step_id: impl Into<String>) -> Self {
self.dependencies.push(step_id.into());
self
}
pub fn mark_in_progress(&mut self) {
self.status = StepStatus::InProgress;
}
pub fn mark_complete(&mut self, result: Option<Value>) {
self.status = StepStatus::Completed;
self.result = result;
}
pub fn mark_failed(&mut self, reason: impl Into<String>) {
self.status = StepStatus::Failed(reason.into());
}
pub fn mark_skipped(&mut self, reason: impl Into<String>) {
self.status = StepStatus::Skipped(reason.into());
}
pub fn is_ready(&self, completed_ids: &HashSet<String>) -> bool {
self.status == StepStatus::Pending
&& self
.dependencies
.iter()
.all(|dep| completed_ids.contains(dep))
}
pub fn to_json(&self) -> Value {
serde_json::to_value(self).unwrap_or(Value::Null)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanProgress {
pub total: usize,
pub completed: usize,
pub failed: usize,
pub skipped: usize,
pub in_progress: usize,
pub pending: usize,
}
impl PlanProgress {
pub fn completion_rate(&self) -> f64 {
if self.total == 0 {
return 0.0;
}
(self.completed + self.failed + self.skipped) as f64 / self.total as f64
}
pub fn success_rate(&self) -> f64 {
let terminal = self.completed + self.failed + self.skipped;
if terminal == 0 {
return 0.0;
}
self.completed as f64 / terminal as f64
}
pub fn to_json(&self) -> Value {
serde_json::to_value(self).unwrap_or(Value::Null)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Plan {
pub name: String,
steps: Vec<PlanStep>,
}
impl Plan {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
steps: Vec::new(),
}
}
pub fn add_step(&mut self, step: PlanStep) -> String {
let id = step.id.clone();
self.steps.push(step);
id
}
pub fn get_step(&self, id: &str) -> Option<&PlanStep> {
self.steps.iter().find(|s| s.id == id)
}
pub fn get_step_mut(&mut self, id: &str) -> Option<&mut PlanStep> {
self.steps.iter_mut().find(|s| s.id == id)
}
pub fn ready_steps(&self) -> Vec<&PlanStep> {
let completed: HashSet<String> = self
.steps
.iter()
.filter(|s| s.status.is_success())
.map(|s| s.id.clone())
.collect();
self.steps
.iter()
.filter(|s| s.is_ready(&completed))
.collect()
}
pub fn next_step(&self) -> Option<&PlanStep> {
self.ready_steps().into_iter().next()
}
pub fn progress(&self) -> PlanProgress {
let mut p = PlanProgress {
total: self.steps.len(),
completed: 0,
failed: 0,
skipped: 0,
in_progress: 0,
pending: 0,
};
for s in &self.steps {
match &s.status {
StepStatus::Pending => p.pending += 1,
StepStatus::InProgress => p.in_progress += 1,
StepStatus::Completed => p.completed += 1,
StepStatus::Failed(_) => p.failed += 1,
StepStatus::Skipped(_) => p.skipped += 1,
}
}
p
}
pub fn is_complete(&self) -> bool {
self.steps.iter().all(|s| s.status.is_terminal())
}
pub fn is_failed(&self) -> bool {
self.steps
.iter()
.any(|s| matches!(s.status, StepStatus::Failed(_)))
}
pub fn steps(&self) -> &[PlanStep] {
&self.steps
}
pub fn len(&self) -> usize {
self.steps.len()
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub fn to_json(&self) -> Value {
serde_json::to_value(self).unwrap_or(Value::Null)
}
pub fn remove_step(&mut self, step_id: &str) -> Option<PlanStep> {
let pos = self.steps.iter().position(|s| s.id == step_id)?;
let removed = self.steps.remove(pos);
for step in &mut self.steps {
step.dependencies.retain(|d| d != step_id);
}
Some(removed)
}
pub fn insert_after(&mut self, after_id: &str, mut step: PlanStep) -> Option<String> {
let pos = self.steps.iter().position(|s| s.id == after_id)?;
if !step.dependencies.contains(&after_id.to_string()) {
step.dependencies.push(after_id.to_string());
}
let id = step.id.clone();
self.steps.insert(pos + 1, step);
Some(id)
}
}
#[derive(Debug)]
pub struct PlanBuilder {
name: String,
steps: Vec<PlanStep>,
}
impl PlanBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
steps: Vec::new(),
}
}
pub fn step(mut self, description: impl Into<String>) -> Self {
self.steps.push(PlanStep::new(description));
self
}
pub fn step_with_deps(mut self, description: impl Into<String>, deps: Vec<String>) -> Self {
let mut s = PlanStep::new(description);
s.dependencies = deps;
self.steps.push(s);
self
}
pub fn sequential(mut self, descriptions: Vec<String>) -> Self {
let mut prev_id: Option<String> = None;
for desc in descriptions {
let mut step = PlanStep::new(desc);
if let Some(pid) = prev_id {
step.dependencies.push(pid);
}
prev_id = Some(step.id.clone());
self.steps.push(step);
}
self
}
pub fn parallel(mut self, descriptions: Vec<String>) -> Self {
for desc in descriptions {
self.steps.push(PlanStep::new(desc));
}
self
}
pub fn build(self) -> Plan {
let mut plan = Plan::new(self.name);
for step in self.steps {
plan.add_step(step);
}
plan
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EventType {
Started,
Completed,
Failed,
Skipped,
Replanned,
}
impl fmt::Display for EventType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EventType::Started => write!(f, "Started"),
EventType::Completed => write!(f, "Completed"),
EventType::Failed => write!(f, "Failed"),
EventType::Skipped => write!(f, "Skipped"),
EventType::Replanned => write!(f, "Replanned"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionEvent {
pub step_id: String,
pub event_type: EventType,
pub timestamp: String,
pub details: Option<Value>,
}
impl ExecutionEvent {
pub fn to_json(&self) -> Value {
serde_json::to_value(self).unwrap_or(Value::Null)
}
}
pub struct PlanExecutor {
plan: Plan,
events: Vec<ExecutionEvent>,
step_counter: usize,
}
impl PlanExecutor {
pub fn new(plan: Plan) -> Self {
Self {
plan,
events: Vec::new(),
step_counter: 0,
}
}
pub fn start_next(&mut self) -> Option<String> {
let id = self.plan.next_step().map(|s| s.id.clone())?;
if let Some(step) = self.plan.get_step_mut(&id) {
step.mark_in_progress();
}
self.step_counter += 1;
self.events.push(ExecutionEvent {
step_id: id.clone(),
event_type: EventType::Started,
timestamp: self.timestamp(),
details: None,
});
Some(id)
}
pub fn complete_step(&mut self, id: &str, result: Option<Value>) {
if let Some(step) = self.plan.get_step_mut(id) {
step.mark_complete(result.clone());
}
self.events.push(ExecutionEvent {
step_id: id.to_string(),
event_type: EventType::Completed,
timestamp: self.timestamp(),
details: result,
});
}
pub fn fail_step(&mut self, id: &str, reason: impl Into<String>) {
let reason = reason.into();
if let Some(step) = self.plan.get_step_mut(id) {
step.mark_failed(&reason);
}
self.events.push(ExecutionEvent {
step_id: id.to_string(),
event_type: EventType::Failed,
timestamp: self.timestamp(),
details: Some(Value::String(reason)),
});
}
pub fn skip_step(&mut self, id: &str, reason: impl Into<String>) {
let reason = reason.into();
if let Some(step) = self.plan.get_step_mut(id) {
step.mark_skipped(&reason);
}
self.events.push(ExecutionEvent {
step_id: id.to_string(),
event_type: EventType::Skipped,
timestamp: self.timestamp(),
details: Some(Value::String(reason)),
});
}
pub fn current_plan(&self) -> &Plan {
&self.plan
}
pub fn current_plan_mut(&mut self) -> &mut Plan {
&mut self.plan
}
pub fn execution_log(&self) -> &[ExecutionEvent] {
&self.events
}
pub fn elapsed_steps(&self) -> usize {
self.step_counter
}
#[allow(dead_code)]
fn record_replan(&mut self, details: Option<Value>) {
self.events.push(ExecutionEvent {
step_id: String::new(),
event_type: EventType::Replanned,
timestamp: self.timestamp(),
details,
});
}
fn timestamp(&self) -> String {
format!("T{}", self.events.len())
}
}
#[derive(Debug)]
pub struct Replanner {
replan_count: usize,
}
impl Replanner {
pub fn new() -> Self {
Self { replan_count: 0 }
}
pub fn add_steps(&mut self, plan: &mut Plan, steps: Vec<PlanStep>) {
for step in steps {
plan.add_step(step);
}
self.replan_count += 1;
}
pub fn remove_step(&mut self, plan: &mut Plan, step_id: &str) -> Option<PlanStep> {
let removed = plan.remove_step(step_id);
if removed.is_some() {
self.replan_count += 1;
}
removed
}
pub fn insert_after(&mut self, plan: &mut Plan, after_id: &str, step: PlanStep) -> String {
let id = plan.insert_after(after_id, step).unwrap_or_default();
if !id.is_empty() {
self.replan_count += 1;
}
id
}
pub fn replan_count(&self) -> usize {
self.replan_count
}
pub fn to_json(&self) -> Value {
serde_json::json!({ "replan_count": self.replan_count })
}
}
impl Default for Replanner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_step_status_pending_is_not_terminal() {
assert!(!StepStatus::Pending.is_terminal());
}
#[test]
fn test_step_status_in_progress_is_not_terminal() {
assert!(!StepStatus::InProgress.is_terminal());
}
#[test]
fn test_step_status_completed_is_terminal() {
assert!(StepStatus::Completed.is_terminal());
}
#[test]
fn test_step_status_failed_is_terminal() {
assert!(StepStatus::Failed("err".into()).is_terminal());
}
#[test]
fn test_step_status_skipped_is_terminal() {
assert!(StepStatus::Skipped("reason".into()).is_terminal());
}
#[test]
fn test_step_status_completed_is_success() {
assert!(StepStatus::Completed.is_success());
}
#[test]
fn test_step_status_failed_is_not_success() {
assert!(!StepStatus::Failed("err".into()).is_success());
}
#[test]
fn test_step_status_display_pending() {
assert_eq!(StepStatus::Pending.to_string(), "Pending");
}
#[test]
fn test_step_status_display_in_progress() {
assert_eq!(StepStatus::InProgress.to_string(), "InProgress");
}
#[test]
fn test_step_status_display_completed() {
assert_eq!(StepStatus::Completed.to_string(), "Completed");
}
#[test]
fn test_step_status_display_failed() {
assert_eq!(
StepStatus::Failed("oops".into()).to_string(),
"Failed: oops"
);
}
#[test]
fn test_step_status_display_skipped() {
assert_eq!(StepStatus::Skipped("na".into()).to_string(), "Skipped: na");
}
#[test]
fn test_plan_step_new_has_pending_status() {
let s = PlanStep::new("do something");
assert_eq!(s.description, "do something");
assert_eq!(s.status, StepStatus::Pending);
assert!(s.dependencies.is_empty());
assert!(s.result.is_none());
}
#[test]
fn test_plan_step_with_dependency() {
let s = PlanStep::new("step").with_dependency("dep-1");
assert_eq!(s.dependencies, vec!["dep-1".to_string()]);
}
#[test]
fn test_plan_step_chained_dependencies() {
let s = PlanStep::new("step")
.with_dependency("a")
.with_dependency("b");
assert_eq!(s.dependencies.len(), 2);
}
#[test]
fn test_plan_step_mark_in_progress() {
let mut s = PlanStep::new("s");
s.mark_in_progress();
assert_eq!(s.status, StepStatus::InProgress);
}
#[test]
fn test_plan_step_mark_complete_with_result() {
let mut s = PlanStep::new("s");
s.mark_complete(Some(json!(42)));
assert_eq!(s.status, StepStatus::Completed);
assert_eq!(s.result, Some(json!(42)));
}
#[test]
fn test_plan_step_mark_complete_without_result() {
let mut s = PlanStep::new("s");
s.mark_complete(None);
assert!(s.status.is_success());
assert!(s.result.is_none());
}
#[test]
fn test_plan_step_mark_failed() {
let mut s = PlanStep::new("s");
s.mark_failed("boom");
assert_eq!(s.status, StepStatus::Failed("boom".into()));
}
#[test]
fn test_plan_step_mark_skipped() {
let mut s = PlanStep::new("s");
s.mark_skipped("not needed");
assert_eq!(s.status, StepStatus::Skipped("not needed".into()));
}
#[test]
fn test_plan_step_is_ready_no_deps() {
let s = PlanStep::new("s");
assert!(s.is_ready(&HashSet::new()));
}
#[test]
fn test_plan_step_is_ready_deps_met() {
let s = PlanStep::new("s").with_dependency("a");
let mut completed = HashSet::new();
completed.insert("a".to_string());
assert!(s.is_ready(&completed));
}
#[test]
fn test_plan_step_not_ready_deps_unmet() {
let s = PlanStep::new("s").with_dependency("a");
assert!(!s.is_ready(&HashSet::new()));
}
#[test]
fn test_plan_step_not_ready_if_in_progress() {
let mut s = PlanStep::new("s");
s.mark_in_progress();
assert!(!s.is_ready(&HashSet::new()));
}
#[test]
fn test_plan_step_to_json() {
let s = PlanStep::new("x");
let v = s.to_json();
assert_eq!(v["description"], "x");
}
#[test]
fn test_plan_step_metadata() {
let mut s = PlanStep::new("s");
s.metadata.insert("key".into(), json!("value"));
assert_eq!(s.metadata["key"], json!("value"));
}
#[test]
fn test_plan_step_estimated_tokens() {
let mut s = PlanStep::new("s");
s.estimated_tokens = Some(500);
assert_eq!(s.estimated_tokens, Some(500));
}
#[test]
fn test_plan_new_is_empty() {
let p = Plan::new("test plan");
assert_eq!(p.name, "test plan");
assert_eq!(p.len(), 0);
assert!(p.is_empty());
}
#[test]
fn test_plan_add_and_get_step() {
let mut p = Plan::new("p");
let s = PlanStep::new("a");
let id = p.add_step(s);
assert_eq!(p.len(), 1);
assert!(p.get_step(&id).is_some());
}
#[test]
fn test_plan_get_step_mut() {
let mut p = Plan::new("p");
let s = PlanStep::new("a");
let id = p.add_step(s);
p.get_step_mut(&id).unwrap().mark_in_progress();
assert_eq!(p.get_step(&id).unwrap().status, StepStatus::InProgress);
}
#[test]
fn test_plan_get_step_nonexistent() {
let p = Plan::new("p");
assert!(p.get_step("nope").is_none());
}
#[test]
fn test_plan_ready_steps_all_independent() {
let mut p = Plan::new("p");
p.add_step(PlanStep::new("a"));
p.add_step(PlanStep::new("b"));
assert_eq!(p.ready_steps().len(), 2);
}
#[test]
fn test_plan_ready_steps_with_dependency() {
let mut p = Plan::new("p");
let id_a = p.add_step(PlanStep::new("a"));
p.add_step(PlanStep::new("b").with_dependency(id_a));
assert_eq!(p.ready_steps().len(), 1);
assert_eq!(p.ready_steps()[0].description, "a");
}
#[test]
fn test_plan_next_step() {
let mut p = Plan::new("p");
p.add_step(PlanStep::new("first"));
let ns = p.next_step().unwrap();
assert_eq!(ns.description, "first");
}
#[test]
fn test_plan_next_step_empty() {
let p = Plan::new("p");
assert!(p.next_step().is_none());
}
#[test]
fn test_plan_progress() {
let mut p = Plan::new("p");
let id = p.add_step(PlanStep::new("a"));
p.add_step(PlanStep::new("b"));
p.get_step_mut(&id).unwrap().mark_complete(None);
let prog = p.progress();
assert_eq!(prog.total, 2);
assert_eq!(prog.completed, 1);
assert_eq!(prog.pending, 1);
}
#[test]
fn test_plan_is_complete() {
let mut p = Plan::new("p");
let id = p.add_step(PlanStep::new("a"));
assert!(!p.is_complete());
p.get_step_mut(&id).unwrap().mark_complete(None);
assert!(p.is_complete());
}
#[test]
fn test_plan_is_failed() {
let mut p = Plan::new("p");
let id = p.add_step(PlanStep::new("a"));
assert!(!p.is_failed());
p.get_step_mut(&id).unwrap().mark_failed("err");
assert!(p.is_failed());
}
#[test]
fn test_plan_steps_iterator() {
let mut p = Plan::new("p");
p.add_step(PlanStep::new("a"));
p.add_step(PlanStep::new("b"));
assert_eq!(p.steps().len(), 2);
}
#[test]
fn test_plan_to_json() {
let p = Plan::new("my plan");
let v = p.to_json();
assert_eq!(v["name"], "my plan");
}
#[test]
fn test_plan_remove_step() {
let mut p = Plan::new("p");
let id_a = p.add_step(PlanStep::new("a"));
let _id_b = p.add_step(PlanStep::new("b").with_dependency(id_a.clone()));
let removed = p.remove_step(&id_a);
assert!(removed.is_some());
assert_eq!(p.len(), 1);
assert!(p.steps()[0].dependencies.is_empty());
}
#[test]
fn test_plan_remove_step_nonexistent() {
let mut p = Plan::new("p");
assert!(p.remove_step("nope").is_none());
}
#[test]
fn test_plan_insert_after() {
let mut p = Plan::new("p");
let id_a = p.add_step(PlanStep::new("a"));
p.add_step(PlanStep::new("c"));
let new_step = PlanStep::new("b");
let id_b = p.insert_after(&id_a, new_step).unwrap();
assert_eq!(p.len(), 3);
assert_eq!(p.steps()[1].id, id_b);
assert!(p.steps()[1].dependencies.contains(&id_a));
}
#[test]
fn test_plan_insert_after_nonexistent() {
let mut p = Plan::new("p");
let result = p.insert_after("nope", PlanStep::new("x"));
assert!(result.is_none());
}
#[test]
fn test_plan_progress_completion_rate() {
let prog = PlanProgress {
total: 4,
completed: 2,
failed: 1,
skipped: 0,
in_progress: 0,
pending: 1,
};
assert!((prog.completion_rate() - 0.75).abs() < f64::EPSILON);
}
#[test]
fn test_plan_progress_completion_rate_empty() {
let prog = PlanProgress {
total: 0,
completed: 0,
failed: 0,
skipped: 0,
in_progress: 0,
pending: 0,
};
assert!((prog.completion_rate() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_plan_progress_success_rate() {
let prog = PlanProgress {
total: 4,
completed: 2,
failed: 1,
skipped: 1,
in_progress: 0,
pending: 0,
};
assert!((prog.success_rate() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_plan_progress_success_rate_no_terminal() {
let prog = PlanProgress {
total: 2,
completed: 0,
failed: 0,
skipped: 0,
in_progress: 2,
pending: 0,
};
assert!((prog.success_rate() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_plan_progress_to_json() {
let prog = PlanProgress {
total: 1,
completed: 1,
failed: 0,
skipped: 0,
in_progress: 0,
pending: 0,
};
let v = prog.to_json();
assert_eq!(v["total"], 1);
assert_eq!(v["completed"], 1);
}
#[test]
fn test_builder_step() {
let plan = PlanBuilder::new("bp").step("a").step("b").build();
assert_eq!(plan.len(), 2);
assert_eq!(plan.name, "bp");
}
#[test]
fn test_builder_step_with_deps() {
let plan = PlanBuilder::new("bp").step("a").build();
let dep_id = plan.steps()[0].id.clone();
let plan = PlanBuilder::new("bp")
.step_with_deps("b", vec![dep_id.clone()])
.build();
assert_eq!(plan.steps()[0].dependencies, vec![dep_id]);
}
#[test]
fn test_builder_sequential() {
let plan = PlanBuilder::new("seq")
.sequential(vec!["a".into(), "b".into(), "c".into()])
.build();
assert_eq!(plan.len(), 3);
assert!(plan.steps()[0].dependencies.is_empty());
assert_eq!(plan.steps()[1].dependencies.len(), 1);
assert_eq!(plan.steps()[2].dependencies.len(), 1);
assert_eq!(plan.steps()[1].dependencies[0], plan.steps()[0].id);
assert_eq!(plan.steps()[2].dependencies[0], plan.steps()[1].id);
}
#[test]
fn test_builder_parallel() {
let plan = PlanBuilder::new("par")
.parallel(vec!["a".into(), "b".into(), "c".into()])
.build();
assert_eq!(plan.len(), 3);
for s in plan.steps() {
assert!(s.dependencies.is_empty());
}
}
#[test]
fn test_builder_mixed() {
let plan = PlanBuilder::new("mix")
.step("independent")
.sequential(vec!["s1".into(), "s2".into()])
.parallel(vec!["p1".into(), "p2".into()])
.build();
assert_eq!(plan.len(), 5);
}
#[test]
fn test_builder_empty() {
let plan = PlanBuilder::new("empty").build();
assert!(plan.is_empty());
}
#[test]
fn test_event_type_display() {
assert_eq!(EventType::Started.to_string(), "Started");
assert_eq!(EventType::Completed.to_string(), "Completed");
assert_eq!(EventType::Failed.to_string(), "Failed");
assert_eq!(EventType::Skipped.to_string(), "Skipped");
assert_eq!(EventType::Replanned.to_string(), "Replanned");
}
#[test]
fn test_execution_event_to_json() {
let ev = ExecutionEvent {
step_id: "s1".into(),
event_type: EventType::Started,
timestamp: "T0".into(),
details: None,
};
let v = ev.to_json();
assert_eq!(v["step_id"], "s1");
assert_eq!(v["event_type"], "Started");
}
#[test]
fn test_executor_start_next() {
let plan = PlanBuilder::new("e").step("a").step("b").build();
let mut exec = PlanExecutor::new(plan);
let id = exec.start_next().unwrap();
assert_eq!(
exec.current_plan().get_step(&id).unwrap().status,
StepStatus::InProgress
);
assert_eq!(exec.elapsed_steps(), 1);
}
#[test]
fn test_executor_complete_step() {
let plan = PlanBuilder::new("e").step("a").build();
let mut exec = PlanExecutor::new(plan);
let id = exec.start_next().unwrap();
exec.complete_step(&id, Some(json!("done")));
assert!(exec
.current_plan()
.get_step(&id)
.unwrap()
.status
.is_success());
}
#[test]
fn test_executor_fail_step() {
let plan = PlanBuilder::new("e").step("a").build();
let mut exec = PlanExecutor::new(plan);
let id = exec.start_next().unwrap();
exec.fail_step(&id, "oops");
assert!(exec
.current_plan()
.get_step(&id)
.unwrap()
.status
.is_terminal());
assert!(exec.current_plan().is_failed());
}
#[test]
fn test_executor_skip_step() {
let plan = PlanBuilder::new("e").step("a").build();
let mut exec = PlanExecutor::new(plan);
let id = exec.start_next().unwrap();
exec.skip_step(&id, "not needed");
let status = &exec.current_plan().get_step(&id).unwrap().status;
assert!(matches!(status, StepStatus::Skipped(_)));
}
#[test]
fn test_executor_execution_log() {
let plan = PlanBuilder::new("e").step("a").build();
let mut exec = PlanExecutor::new(plan);
let id = exec.start_next().unwrap();
exec.complete_step(&id, None);
assert_eq!(exec.execution_log().len(), 2); }
#[test]
fn test_executor_sequential_flow() {
let plan = PlanBuilder::new("e")
.sequential(vec!["a".into(), "b".into()])
.build();
let mut exec = PlanExecutor::new(plan);
let id_a = exec.start_next().unwrap();
exec.complete_step(&id_a, None);
let id_b = exec.start_next().unwrap();
assert_ne!(id_a, id_b);
exec.complete_step(&id_b, None);
assert!(exec.current_plan().is_complete());
}
#[test]
fn test_executor_no_ready_step() {
let plan = PlanBuilder::new("e")
.sequential(vec!["a".into(), "b".into()])
.build();
let mut exec = PlanExecutor::new(plan);
let id_a = exec.start_next().unwrap();
assert!(exec.start_next().is_none());
exec.complete_step(&id_a, None);
assert!(exec.start_next().is_some());
}
#[test]
fn test_executor_empty_plan() {
let plan = Plan::new("empty");
let mut exec = PlanExecutor::new(plan);
assert!(exec.start_next().is_none());
assert_eq!(exec.elapsed_steps(), 0);
}
#[test]
fn test_executor_elapsed_steps() {
let plan = PlanBuilder::new("e")
.parallel(vec!["a".into(), "b".into()])
.build();
let mut exec = PlanExecutor::new(plan);
let id1 = exec.start_next().unwrap();
exec.complete_step(&id1, None);
let id2 = exec.start_next().unwrap();
exec.complete_step(&id2, None);
assert_eq!(exec.elapsed_steps(), 2);
}
#[test]
fn test_replanner_add_steps() {
let mut plan = PlanBuilder::new("p").step("a").build();
let mut rp = Replanner::new();
rp.add_steps(&mut plan, vec![PlanStep::new("b"), PlanStep::new("c")]);
assert_eq!(plan.len(), 3);
assert_eq!(rp.replan_count(), 1);
}
#[test]
fn test_replanner_remove_step() {
let mut plan = PlanBuilder::new("p").step("a").step("b").build();
let id = plan.steps()[0].id.clone();
let mut rp = Replanner::new();
let removed = rp.remove_step(&mut plan, &id);
assert!(removed.is_some());
assert_eq!(plan.len(), 1);
assert_eq!(rp.replan_count(), 1);
}
#[test]
fn test_replanner_remove_nonexistent() {
let mut plan = Plan::new("p");
let mut rp = Replanner::new();
assert!(rp.remove_step(&mut plan, "nope").is_none());
assert_eq!(rp.replan_count(), 0);
}
#[test]
fn test_replanner_insert_after() {
let mut plan = PlanBuilder::new("p").step("a").step("c").build();
let id_a = plan.steps()[0].id.clone();
let mut rp = Replanner::new();
let id_b = rp.insert_after(&mut plan, &id_a, PlanStep::new("b"));
assert!(!id_b.is_empty());
assert_eq!(plan.len(), 3);
assert_eq!(plan.steps()[1].description, "b");
assert_eq!(rp.replan_count(), 1);
}
#[test]
fn test_replanner_to_json() {
let rp = Replanner::new();
let v = rp.to_json();
assert_eq!(v["replan_count"], 0);
}
#[test]
fn test_replanner_default() {
let rp = Replanner::default();
assert_eq!(rp.replan_count(), 0);
}
#[test]
fn test_replanner_multiple_operations() {
let mut plan = PlanBuilder::new("p").step("a").build();
let mut rp = Replanner::new();
rp.add_steps(&mut plan, vec![PlanStep::new("b")]);
rp.add_steps(&mut plan, vec![PlanStep::new("c")]);
let id = plan.steps()[1].id.clone();
rp.remove_step(&mut plan, &id);
assert_eq!(rp.replan_count(), 3);
}
#[test]
fn test_full_plan_execution() {
let plan = PlanBuilder::new("deploy")
.sequential(vec!["build".into(), "test".into(), "deploy".into()])
.build();
let mut exec = PlanExecutor::new(plan);
for _ in 0..3 {
let id = exec.start_next().unwrap();
exec.complete_step(&id, Some(json!("ok")));
}
assert!(exec.current_plan().is_complete());
assert!(!exec.current_plan().is_failed());
let prog = exec.current_plan().progress();
assert_eq!(prog.completed, 3);
assert!((prog.completion_rate() - 1.0).abs() < f64::EPSILON);
assert!((prog.success_rate() - 1.0).abs() < f64::EPSILON);
assert_eq!(exec.execution_log().len(), 6); }
#[test]
fn test_plan_with_failure_and_replan() {
let plan = PlanBuilder::new("p")
.sequential(vec!["a".into(), "b".into()])
.build();
let mut exec = PlanExecutor::new(plan);
let id_a = exec.start_next().unwrap();
exec.fail_step(&id_a, "timeout");
let mut rp = Replanner::new();
let retry = PlanStep::new("retry-a");
rp.add_steps(exec.current_plan_mut(), vec![retry]);
let id_retry = exec.start_next().unwrap();
exec.complete_step(&id_retry, None);
assert!(exec.current_plan().is_failed()); assert_eq!(rp.replan_count(), 1);
}
#[test]
fn test_parallel_execution_pattern() {
let plan = PlanBuilder::new("p")
.parallel(vec!["fetch-a".into(), "fetch-b".into(), "fetch-c".into()])
.build();
let mut exec = PlanExecutor::new(plan);
let mut ids = Vec::new();
for _ in 0..3 {
if let Some(id) = exec.start_next() {
ids.push(id);
}
}
assert_eq!(ids.len(), 3);
for id in &ids {
exec.complete_step(id, None);
}
assert!(exec.current_plan().is_complete());
}
#[test]
fn test_diamond_dependency() {
let mut plan = Plan::new("diamond");
let step_a = PlanStep::new("A");
let id_a = step_a.id.clone();
plan.add_step(step_a);
let step_b = PlanStep::new("B").with_dependency(id_a.clone());
let id_b = step_b.id.clone();
plan.add_step(step_b);
let step_c = PlanStep::new("C").with_dependency(id_a.clone());
let id_c = step_c.id.clone();
plan.add_step(step_c);
plan.add_step(
PlanStep::new("D")
.with_dependency(id_b.clone())
.with_dependency(id_c.clone()),
);
let mut exec = PlanExecutor::new(plan);
let a = exec.start_next().unwrap();
assert!(exec.start_next().is_none());
exec.complete_step(&a, None);
let b = exec.start_next().unwrap();
let c = exec.start_next().unwrap();
assert!(exec.start_next().is_none()); exec.complete_step(&b, None);
exec.complete_step(&c, None);
let d = exec.start_next().unwrap();
exec.complete_step(&d, None);
assert!(exec.current_plan().is_complete());
}
#[test]
fn test_step_status_serialization_roundtrip() {
let statuses = vec![
StepStatus::Pending,
StepStatus::InProgress,
StepStatus::Completed,
StepStatus::Failed("err".into()),
StepStatus::Skipped("reason".into()),
];
for s in statuses {
let json = serde_json::to_string(&s).unwrap();
let deserialized: StepStatus = serde_json::from_str(&json).unwrap();
assert_eq!(s, deserialized);
}
}
#[test]
fn test_plan_serialization_roundtrip() {
let plan = PlanBuilder::new("test")
.sequential(vec!["a".into(), "b".into()])
.build();
let json = serde_json::to_string(&plan).unwrap();
let deserialized: Plan = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, "test");
assert_eq!(deserialized.len(), 2);
}
}