ainl_mission/
lifecycle.rs1use ainl_contracts::{Mission, MissionId, MissionState};
4use thiserror::Error;
5
6use crate::state_machine::{transition, StateMachineError};
7
8pub trait MissionLifecycle {
10 fn load_mission(&self, mission_id: &MissionId) -> Result<Mission, MissionLifecycleError>;
12
13 fn save_mission(&self, mission: &Mission) -> Result<(), MissionLifecycleError>;
15
16 fn set_state(
18 &self,
19 mission_id: &MissionId,
20 new_state: MissionState,
21 ) -> Result<MissionState, MissionLifecycleError> {
22 let mut mission = self.load_mission(mission_id)?;
23 let next = transition(mission.state, new_state).map_err(MissionLifecycleError::State)?;
24 mission.state = next;
25 self.save_mission(&mission)?;
26 Ok(next)
27 }
28
29 fn initialize(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
31 self.set_state(mission_id, MissionState::Initializing)
32 }
33
34 fn start_running(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
36 let mission = self.load_mission(mission_id)?;
37 let target = match mission.state {
38 MissionState::Initializing | MissionState::Paused | MissionState::OrchestratorTurn => {
39 MissionState::Running
40 }
41 other => other,
42 };
43 self.set_state(mission_id, target)
44 }
45
46 fn pause(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
47 self.set_state(mission_id, MissionState::Paused)
48 }
49
50 fn resume(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
51 self.set_state(mission_id, MissionState::Running)
52 }
53
54 fn enter_orchestrator_turn(
55 &self,
56 mission_id: &MissionId,
57 ) -> Result<MissionState, MissionLifecycleError> {
58 self.set_state(mission_id, MissionState::OrchestratorTurn)
59 }
60
61 fn complete(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
62 self.set_state(mission_id, MissionState::Completed)
63 }
64
65 fn cancel(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
66 self.set_state(mission_id, MissionState::Cancelled)
67 }
68}
69
70#[derive(Debug, Error)]
72pub enum MissionLifecycleError {
73 #[error("state machine: {0}")]
74 State(#[from] StateMachineError),
75 #[error("not found: {0}")]
76 NotFound(String),
77 #[error("persist: {0}")]
78 Persist(String),
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use ainl_contracts::MissionCapabilityFlags;
85 use chrono::Utc;
86 use std::cell::RefCell;
87 use std::collections::HashMap;
88
89 struct MemLifecycle {
90 missions: RefCell<HashMap<String, Mission>>,
91 }
92
93 impl MemLifecycle {
94 fn with_mission(state: MissionState) -> Self {
95 let m = Mission {
96 mission_id: MissionId("m1".into()),
97 objective_md: "test".into(),
98 state,
99 milestone_ids: vec![],
100 mission_root: None,
101 created_at: Utc::now(),
102 last_orchestrator_turn_at: None,
103 capability_flags: MissionCapabilityFlags::default(),
104 };
105 let mut map = HashMap::new();
106 map.insert("m1".into(), m);
107 Self {
108 missions: RefCell::new(map),
109 }
110 }
111 }
112
113 impl MissionLifecycle for MemLifecycle {
114 fn load_mission(&self, mission_id: &MissionId) -> Result<Mission, MissionLifecycleError> {
115 self.missions
116 .borrow()
117 .get(mission_id.as_str())
118 .cloned()
119 .ok_or_else(|| MissionLifecycleError::NotFound(mission_id.as_str().into()))
120 }
121
122 fn save_mission(&self, mission: &Mission) -> Result<(), MissionLifecycleError> {
123 self.missions
124 .borrow_mut()
125 .insert(mission.mission_id.as_str().into(), mission.clone());
126 Ok(())
127 }
128 }
129
130 #[test]
131 fn initialize_then_run() {
132 let lc = MemLifecycle::with_mission(MissionState::AwaitingInput);
133 let id = MissionId("m1".into());
134 assert_eq!(lc.initialize(&id).unwrap(), MissionState::Initializing);
135 assert_eq!(lc.start_running(&id).unwrap(), MissionState::Running);
136 }
137
138 #[test]
139 fn pause_resume() {
140 let lc = MemLifecycle::with_mission(MissionState::Running);
141 let id = MissionId("m1".into());
142 assert_eq!(lc.pause(&id).unwrap(), MissionState::Paused);
143 assert_eq!(lc.resume(&id).unwrap(), MissionState::Running);
144 }
145}