use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use super::{FsmState, StepKind, StepStatus};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Step {
pub id: Uuid,
pub run_id: Uuid,
pub name: String,
pub kind: StepKind,
pub position: u32,
pub status: FsmState<StepStatus>,
pub input: Option<Value>,
pub output: Option<Value>,
pub error: Option<String>,
pub duration_ms: u64,
pub cost_usd: Decimal,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub debug_messages: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewStep {
pub run_id: Uuid,
pub name: String,
pub kind: StepKind,
pub position: u32,
pub input: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StepUpdate {
pub status: Option<StepStatus>,
pub output: Option<Value>,
pub error: Option<String>,
pub duration_ms: Option<u64>,
pub cost_usd: Option<Decimal>,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub debug_messages: Option<Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn newstep_serde_roundtrip() {
let new_step = NewStep {
run_id: Uuid::nil(),
name: "build".to_string(),
kind: StepKind::Shell,
position: 0,
input: Some(json!({"command": "cargo build"})),
};
let json = serde_json::to_string(&new_step).expect("serialize");
let back: NewStep = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.run_id, new_step.run_id);
assert_eq!(back.name, new_step.name);
assert_eq!(back.kind, new_step.kind);
assert_eq!(back.position, new_step.position);
assert_eq!(back.input, new_step.input);
}
#[test]
fn step_serde_preserves_all_fields() {
use crate::entities::FsmState;
use chrono::Utc;
let now = Utc::now();
let step = Step {
id: Uuid::now_v7(),
run_id: Uuid::now_v7(),
name: "test-step".to_string(),
kind: StepKind::Agent,
position: 1,
status: FsmState::new(StepStatus::Completed, Uuid::now_v7()),
input: Some(json!({"input": "data"})),
output: Some(json!({"output": "result"})),
error: None,
duration_ms: 2500,
cost_usd: Decimal::new(150, 2),
input_tokens: Some(100),
output_tokens: Some(200),
created_at: now,
updated_at: now,
started_at: Some(now),
completed_at: Some(now),
debug_messages: None,
};
let json = serde_json::to_string(&step).expect("serialize");
let back: Step = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.id, step.id);
assert_eq!(back.run_id, step.run_id);
assert_eq!(back.name, step.name);
assert_eq!(back.kind, step.kind);
assert_eq!(back.position, step.position);
assert_eq!(back.status.state, step.status.state);
assert_eq!(back.input, step.input);
assert_eq!(back.output, step.output);
assert_eq!(back.error, step.error);
assert_eq!(back.duration_ms, step.duration_ms);
assert_eq!(back.cost_usd, step.cost_usd);
assert_eq!(back.input_tokens, step.input_tokens);
assert_eq!(back.output_tokens, step.output_tokens);
}
#[test]
fn stepupdate_default_is_no_changes() {
let update = StepUpdate::default();
assert!(update.status.is_none());
assert!(update.output.is_none());
assert!(update.error.is_none());
assert!(update.duration_ms.is_none());
assert!(update.cost_usd.is_none());
assert!(update.input_tokens.is_none());
assert!(update.output_tokens.is_none());
assert!(update.started_at.is_none());
assert!(update.completed_at.is_none());
assert!(update.debug_messages.is_none());
}
#[test]
fn stepupdate_serde_roundtrip() {
let update = StepUpdate {
status: Some(StepStatus::Completed),
output: Some(json!({"result": "ok"})),
error: None,
duration_ms: Some(1000),
cost_usd: Some(Decimal::new(50, 2)),
input_tokens: Some(50),
output_tokens: Some(75),
started_at: None,
completed_at: None,
debug_messages: None,
};
let json = serde_json::to_string(&update).expect("serialize");
let back: StepUpdate = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.status, update.status);
assert_eq!(back.output, update.output);
assert_eq!(back.duration_ms, update.duration_ms);
assert_eq!(back.cost_usd, update.cost_usd);
assert_eq!(back.input_tokens, update.input_tokens);
assert_eq!(back.output_tokens, update.output_tokens);
}
}