use crate::types::{AgentId, ProviderId, TenantId};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum OpsEvent {
SupervisorStateChange {
tenant: Option<TenantId>,
agent: AgentId,
state: String,
reason: Option<String>,
},
SupervisorStallDetected {
tenant: Option<TenantId>,
agent: AgentId,
missed_heartbeats: u32,
},
KillSwitchTripped {
tenant: Option<TenantId>,
trigger: String,
reason: String,
},
GovernorDenial {
tenant: Option<TenantId>,
resource: String,
provider: Option<ProviderId>,
host: Option<String>,
reason: String,
},
GovernorFence {
tenant: Option<TenantId>,
scope: String,
reason: Option<String>,
},
GovernorBudgetSnapshot {
tenant: Option<TenantId>,
scope: String,
remaining: i64,
limit: i64,
},
GateDecision {
tenant: Option<TenantId>,
tool: String,
decision: String,
gate: String,
policy_ref: Option<String>,
reason: Option<String>,
},
}
impl OpsEvent {
pub fn into_episode_payload(self) -> Result<serde_json::Value, serde_json::Error> {
serde_json::to_value(self)
}
#[must_use]
pub fn from_episode_payload(v: &serde_json::Value) -> Option<Self> {
serde_json::from_value(v.clone()).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::TenantId;
#[test]
fn supervisor_state_change_round_trips() {
let original = OpsEvent::SupervisorStateChange {
tenant: Some(TenantId::default()),
agent: crate::types::AgentId::default(),
state: "running".into(),
reason: Some("startup".into()),
};
let payload = original.clone().into_episode_payload().expect("serialise");
let recovered = OpsEvent::from_episode_payload(&payload).expect("round-trip");
let original_json = serde_json::to_value(&original).unwrap();
let recovered_json = serde_json::to_value(&recovered).unwrap();
assert_eq!(original_json, recovered_json);
}
#[test]
fn discriminator_is_snake_case() {
let event = OpsEvent::KillSwitchTripped {
tenant: None,
trigger: "programmatic".into(),
reason: "operator initiated".into(),
};
let payload = event.into_episode_payload().expect("serialise");
assert_eq!(
payload.get("kind").and_then(|v| v.as_str()),
Some("kill_switch_tripped"),
"serde rename_all = snake_case must produce snake_case discriminator"
);
}
}