Skip to main content

solti_model/resource/
status.rs

1//! # Task status.
2//!
3//! [`TaskStatus`] tracks observed state: phase, attempt count, exit code, last error.
4
5use serde::{Deserialize, Serialize};
6
7use crate::TaskPhase;
8
9/// Observed runtime state of a task.
10///
11/// ## Also
12///
13/// - [`TaskPhase`] lifecycle phase enum.
14/// - [`Task`](crate::Task) aggregate that embeds `TaskStatus`.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct TaskStatus {
18    /// Current lifecycle phase.
19    pub phase: TaskPhase,
20    /// Number of execution attempts.
21    pub attempt: u32,
22    /// Process exit code (Subprocess/Container only).
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub exit_code: Option<i32>,
25    /// Last error message (present when phase is Failed/Timeout).
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub error: Option<String>,
28}
29
30impl TaskStatus {
31    /// Create initial pending status.
32    pub fn pending() -> Self {
33        Self {
34            phase: TaskPhase::Pending,
35            exit_code: None,
36            error: None,
37            attempt: 0,
38        }
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn pending_default() {
48        let s = TaskStatus::pending();
49        assert_eq!(s.phase, TaskPhase::Pending);
50        assert_eq!(s.attempt, 0);
51        assert!(s.error.is_none());
52    }
53
54    #[test]
55    fn error_stored() {
56        let s = TaskStatus {
57            phase: TaskPhase::Failed,
58            attempt: 3,
59            exit_code: None,
60            error: Some("timeout".into()),
61        };
62        assert_eq!(s.error.as_deref(), Some("timeout"));
63    }
64
65    #[test]
66    fn serde_skips_none_error() {
67        let s = TaskStatus::pending();
68        let json = serde_json::to_string(&s).unwrap();
69        assert!(!json.contains("error"));
70    }
71
72    #[test]
73    fn serde_roundtrip() {
74        let s = TaskStatus {
75            phase: TaskPhase::Running,
76            attempt: 2,
77            exit_code: None,
78            error: None,
79        };
80        let json = serde_json::to_string(&s).unwrap();
81        let back: TaskStatus = serde_json::from_str(&json).unwrap();
82        assert_eq!(back.phase, TaskPhase::Running);
83        assert_eq!(back.attempt, 2);
84    }
85
86    #[test]
87    fn exit_code_serde() {
88        let s = TaskStatus {
89            phase: TaskPhase::Failed,
90            attempt: 1,
91            exit_code: Some(137),
92            error: Some("killed".into()),
93        };
94        let json = serde_json::to_string(&s).unwrap();
95        assert!(json.contains("\"exitCode\":137"));
96
97        let back: TaskStatus = serde_json::from_str(&json).unwrap();
98        assert_eq!(back.exit_code, Some(137));
99    }
100
101    #[test]
102    fn serde_skips_none_exit_code() {
103        let s = TaskStatus::pending();
104        let json = serde_json::to_string(&s).unwrap();
105        assert!(!json.contains("exitCode"));
106    }
107}