ainl-mission 0.1.0

Host-neutral mission engine: state machine, DAG, scheduler, stall, task ledger (zero armaraos-* deps)
Documentation
//! [`MissionEventSink`] — persist and broadcast mission progress events.

use ainl_contracts::{MissionEvent, MissionId, MissionState};
use thiserror::Error;

/// Host implements persistence (DB rows, SSE, audit).
pub trait MissionEventSink {
    fn emit(&self, event: &MissionEvent) -> Result<(), MissionEventError>;

    /// Convenience: state transition event.
    fn emit_state_changed(
        &self,
        mission_id: &MissionId,
        from: MissionState,
        to: MissionState,
    ) -> Result<(), MissionEventError> {
        self.emit(&MissionEvent::MissionStateChanged {
            mission_id: mission_id.clone(),
            from,
            to,
        })
    }
}

/// Event sink failure.
#[derive(Debug, Error)]
pub enum MissionEventError {
    #[error("emit failed: {0}")]
    Emit(String),
}

#[cfg(test)]
mod tests {
    use super::*;
    use ainl_contracts::MissionEvent;
    use std::cell::RefCell;

    struct VecSink(RefCell<Vec<MissionEvent>>);

    impl MissionEventSink for VecSink {
        fn emit(&self, event: &MissionEvent) -> Result<(), MissionEventError> {
            self.0.borrow_mut().push(event.clone());
            Ok(())
        }
    }

    #[test]
    fn state_changed_helper() {
        let sink = VecSink(RefCell::new(Vec::new()));
        let id = MissionId("m1".into());
        sink.emit_state_changed(&id, MissionState::Running, MissionState::Paused)
            .unwrap();
        let events = sink.0.borrow();
        assert!(matches!(
            events[0],
            MissionEvent::MissionStateChanged { .. }
        ));
    }
}