agentstategraph_core/
lifecycle.rs1use crate::intent::{IntentLifecycle, IntentStatus, Resolution};
9
10#[derive(Debug, Clone, PartialEq, thiserror::Error)]
12pub enum LifecycleError {
13 #[error("invalid transition from {from:?} to {to:?}")]
14 InvalidTransition {
15 from: IntentStatus,
16 to: IntentStatus,
17 },
18 #[error("resolution required to transition to {0:?}")]
19 ResolutionRequired(IntentStatus),
20}
21
22pub fn is_valid_transition(from: &IntentStatus, to: &IntentStatus) -> bool {
24 matches!(
25 (from, to),
26 (IntentStatus::Proposed, IntentStatus::Authorized)
27 | (IntentStatus::Proposed, IntentStatus::InProgress) | (IntentStatus::Authorized, IntentStatus::InProgress)
29 | (IntentStatus::InProgress, IntentStatus::Completed)
30 | (IntentStatus::InProgress, IntentStatus::Failed)
31 | (IntentStatus::InProgress, IntentStatus::Blocked)
32 | (IntentStatus::Blocked, IntentStatus::InProgress)
33 | (IntentStatus::Blocked, IntentStatus::Failed)
34 )
35}
36
37pub fn transition(
40 lifecycle: &mut IntentLifecycle,
41 to: IntentStatus,
42 resolution: Option<Resolution>,
43) -> Result<(), LifecycleError> {
44 if !is_valid_transition(&lifecycle.status, &to) {
45 return Err(LifecycleError::InvalidTransition {
46 from: lifecycle.status.clone(),
47 to,
48 });
49 }
50
51 match &to {
53 IntentStatus::Completed | IntentStatus::Failed => {
54 if resolution.is_none() && lifecycle.resolution.is_none() {
55 return Err(LifecycleError::ResolutionRequired(to));
56 }
57 if let Some(res) = resolution {
58 lifecycle.resolution = Some(res);
59 }
60 }
61 _ => {}
62 }
63
64 lifecycle.status = to;
65 Ok(())
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::intent::{Outcome, Resolution};
72
73 fn test_lifecycle() -> IntentLifecycle {
74 IntentLifecycle {
75 status: IntentStatus::Proposed,
76 assigned_to: vec![],
77 resolution: None,
78 notification: None,
79 }
80 }
81
82 fn test_resolution() -> Resolution {
83 Resolution {
84 summary: "Done".to_string(),
85 deviations: vec![],
86 commits: vec![],
87 branches_explored: vec![],
88 outcome: Outcome::Fulfilled,
89 confidence: 0.9,
90 }
91 }
92
93 #[test]
94 fn test_valid_happy_path() {
95 let mut lc = test_lifecycle();
96 assert!(transition(&mut lc, IntentStatus::Authorized, None).is_ok());
97 assert!(transition(&mut lc, IntentStatus::InProgress, None).is_ok());
98 assert!(transition(&mut lc, IntentStatus::Completed, Some(test_resolution())).is_ok());
99 assert_eq!(lc.status, IntentStatus::Completed);
100 assert!(lc.resolution.is_some());
101 }
102
103 #[test]
104 fn test_skip_auth() {
105 let mut lc = test_lifecycle();
106 assert!(transition(&mut lc, IntentStatus::InProgress, None).is_ok());
107 }
108
109 #[test]
110 fn test_blocked_and_resume() {
111 let mut lc = test_lifecycle();
112 transition(&mut lc, IntentStatus::InProgress, None).unwrap();
113 transition(&mut lc, IntentStatus::Blocked, None).unwrap();
114 transition(&mut lc, IntentStatus::InProgress, None).unwrap();
115 assert_eq!(lc.status, IntentStatus::InProgress);
116 }
117
118 #[test]
119 fn test_invalid_transition() {
120 let mut lc = test_lifecycle();
121 assert!(transition(&mut lc, IntentStatus::Completed, Some(test_resolution())).is_err());
122 }
123
124 #[test]
125 fn test_completed_requires_resolution() {
126 let mut lc = test_lifecycle();
127 transition(&mut lc, IntentStatus::InProgress, None).unwrap();
128 assert!(transition(&mut lc, IntentStatus::Completed, None).is_err());
129 }
130
131 #[test]
132 fn test_failed_requires_resolution() {
133 let mut lc = test_lifecycle();
134 transition(&mut lc, IntentStatus::InProgress, None).unwrap();
135 assert!(transition(&mut lc, IntentStatus::Failed, None).is_err());
136 }
137
138 #[test]
139 fn test_cannot_go_backwards() {
140 let mut lc = test_lifecycle();
141 transition(&mut lc, IntentStatus::Authorized, None).unwrap();
142 transition(&mut lc, IntentStatus::InProgress, None).unwrap();
143 assert!(transition(&mut lc, IntentStatus::Proposed, None).is_err());
144 assert!(transition(&mut lc, IntentStatus::Authorized, None).is_err());
145 }
146}