1use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use uuid::Uuid;
8
9use super::{FsmState, StepKind, StepStatus};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
23#[non_exhaustive]
24pub struct Step {
25 pub id: Uuid,
27 pub run_id: Uuid,
29 pub name: String,
31 pub kind: StepKind,
33 pub position: u32,
40 pub status: FsmState<StepStatus>,
42 pub input: Option<Value>,
44 pub output: Option<Value>,
46 pub error: Option<String>,
48 pub duration_ms: u64,
50 pub cost_usd: Decimal,
52 pub input_tokens: Option<u64>,
54 pub output_tokens: Option<u64>,
56 pub created_at: DateTime<Utc>,
58 pub updated_at: DateTime<Utc>,
60 pub started_at: Option<DateTime<Utc>>,
62 pub completed_at: Option<DateTime<Utc>>,
64 pub debug_messages: Option<Value>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct NewStep {
87 pub run_id: Uuid,
89 pub name: String,
91 pub kind: StepKind,
93 pub position: u32,
95 pub input: Option<Value>,
97}
98
99#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116pub struct StepUpdate {
117 pub status: Option<StepStatus>,
119 pub output: Option<Value>,
121 pub error: Option<String>,
123 pub duration_ms: Option<u64>,
125 pub cost_usd: Option<Decimal>,
127 pub input_tokens: Option<u64>,
129 pub output_tokens: Option<u64>,
131 pub started_at: Option<DateTime<Utc>>,
133 pub completed_at: Option<DateTime<Utc>>,
135 pub debug_messages: Option<Value>,
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use serde_json::json;
143
144 #[test]
145 fn newstep_serde_roundtrip() {
146 let new_step = NewStep {
147 run_id: Uuid::nil(),
148 name: "build".to_string(),
149 kind: StepKind::Shell,
150 position: 0,
151 input: Some(json!({"command": "cargo build"})),
152 };
153
154 let json = serde_json::to_string(&new_step).expect("serialize");
155 let back: NewStep = serde_json::from_str(&json).expect("deserialize");
156
157 assert_eq!(back.run_id, new_step.run_id);
158 assert_eq!(back.name, new_step.name);
159 assert_eq!(back.kind, new_step.kind);
160 assert_eq!(back.position, new_step.position);
161 assert_eq!(back.input, new_step.input);
162 }
163
164 #[test]
165 fn step_serde_preserves_all_fields() {
166 use crate::entities::FsmState;
167 use chrono::Utc;
168
169 let now = Utc::now();
170 let step = Step {
171 id: Uuid::now_v7(),
172 run_id: Uuid::now_v7(),
173 name: "test-step".to_string(),
174 kind: StepKind::Agent,
175 position: 1,
176 status: FsmState::new(StepStatus::Completed, Uuid::now_v7()),
177 input: Some(json!({"input": "data"})),
178 output: Some(json!({"output": "result"})),
179 error: None,
180 duration_ms: 2500,
181 cost_usd: Decimal::new(150, 2),
182 input_tokens: Some(100),
183 output_tokens: Some(200),
184 created_at: now,
185 updated_at: now,
186 started_at: Some(now),
187 completed_at: Some(now),
188 debug_messages: None,
189 };
190
191 let json = serde_json::to_string(&step).expect("serialize");
192 let back: Step = serde_json::from_str(&json).expect("deserialize");
193
194 assert_eq!(back.id, step.id);
195 assert_eq!(back.run_id, step.run_id);
196 assert_eq!(back.name, step.name);
197 assert_eq!(back.kind, step.kind);
198 assert_eq!(back.position, step.position);
199 assert_eq!(back.status.state, step.status.state);
200 assert_eq!(back.input, step.input);
201 assert_eq!(back.output, step.output);
202 assert_eq!(back.error, step.error);
203 assert_eq!(back.duration_ms, step.duration_ms);
204 assert_eq!(back.cost_usd, step.cost_usd);
205 assert_eq!(back.input_tokens, step.input_tokens);
206 assert_eq!(back.output_tokens, step.output_tokens);
207 }
208
209 #[test]
210 fn stepupdate_default_is_no_changes() {
211 let update = StepUpdate::default();
212 assert!(update.status.is_none());
213 assert!(update.output.is_none());
214 assert!(update.error.is_none());
215 assert!(update.duration_ms.is_none());
216 assert!(update.cost_usd.is_none());
217 assert!(update.input_tokens.is_none());
218 assert!(update.output_tokens.is_none());
219 assert!(update.started_at.is_none());
220 assert!(update.completed_at.is_none());
221 assert!(update.debug_messages.is_none());
222 }
223
224 #[test]
225 fn stepupdate_serde_roundtrip() {
226 let update = StepUpdate {
227 status: Some(StepStatus::Completed),
228 output: Some(json!({"result": "ok"})),
229 error: None,
230 duration_ms: Some(1000),
231 cost_usd: Some(Decimal::new(50, 2)),
232 input_tokens: Some(50),
233 output_tokens: Some(75),
234 started_at: None,
235 completed_at: None,
236 debug_messages: None,
237 };
238
239 let json = serde_json::to_string(&update).expect("serialize");
240 let back: StepUpdate = serde_json::from_str(&json).expect("deserialize");
241
242 assert_eq!(back.status, update.status);
243 assert_eq!(back.output, update.output);
244 assert_eq!(back.duration_ms, update.duration_ms);
245 assert_eq!(back.cost_usd, update.cost_usd);
246 assert_eq!(back.input_tokens, update.input_tokens);
247 assert_eq!(back.output_tokens, update.output_tokens);
248 }
249}