use crate::core::types::ProvenanceEvent;
use crate::tripwire::eventlog;
use std::path::Path;
pub struct PromotionParams<'a> {
pub state_dir: &'a Path,
pub target_env: &'a str,
pub source: &'a str,
pub target: &'a str,
pub gates_passed: u32,
pub gates_total: u32,
pub rollout_strategy: Option<&'a str>,
}
pub fn log_promotion(params: &PromotionParams<'_>) -> Result<(), String> {
let event = ProvenanceEvent::PromotionCompleted {
source: params.source.to_string(),
target: params.target.to_string(),
success: true,
gates_passed: params.gates_passed,
gates_total: params.gates_total,
rollout_strategy: params.rollout_strategy.map(String::from),
};
eventlog::append_event(params.state_dir, params.target_env, event)
}
pub fn log_promotion_failure(params: &PromotionParams<'_>) -> Result<(), String> {
let event = ProvenanceEvent::PromotionCompleted {
source: params.source.to_string(),
target: params.target.to_string(),
success: false,
gates_passed: params.gates_passed,
gates_total: params.gates_total,
rollout_strategy: None,
};
eventlog::append_event(params.state_dir, params.target_env, event)
}
pub fn log_rollback(
state_dir: &Path,
environment: &str,
failed_step: usize,
reason: &str,
) -> Result<(), String> {
let event = ProvenanceEvent::RollbackTriggered {
environment: environment.to_string(),
failed_step,
reason: reason.to_string(),
};
eventlog::append_event(state_dir, environment, event)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log_promotion_success() {
let dir = tempfile::tempdir().unwrap();
let state_dir = dir.path().join("state");
std::fs::create_dir_all(&state_dir).unwrap();
let params = PromotionParams {
state_dir: &state_dir,
target_env: "staging",
source: "dev",
target: "staging",
gates_passed: 3,
gates_total: 3,
rollout_strategy: Some("canary"),
};
let result = log_promotion(¶ms);
assert!(result.is_ok());
let log_path = state_dir.join("staging").join("events.jsonl");
let content = std::fs::read_to_string(&log_path).unwrap();
assert!(content.contains("promotion_completed"));
assert!(content.contains("\"success\":true"));
assert!(content.contains("\"source\":\"dev\""));
assert!(content.contains("\"target\":\"staging\""));
assert!(content.contains("\"gates_passed\":3"));
assert!(content.contains("canary"));
}
#[test]
fn log_promotion_fail() {
let dir = tempfile::tempdir().unwrap();
let state_dir = dir.path().join("state");
std::fs::create_dir_all(&state_dir).unwrap();
let params = PromotionParams {
state_dir: &state_dir,
target_env: "prod",
source: "staging",
target: "prod",
gates_passed: 1,
gates_total: 3,
rollout_strategy: None,
};
let result = log_promotion_failure(¶ms);
assert!(result.is_ok());
let log_path = state_dir.join("prod").join("events.jsonl");
let content = std::fs::read_to_string(&log_path).unwrap();
assert!(content.contains("\"success\":false"));
assert!(content.contains("\"gates_passed\":1"));
}
#[test]
fn log_rollback_event() {
let dir = tempfile::tempdir().unwrap();
let state_dir = dir.path().join("state");
std::fs::create_dir_all(&state_dir).unwrap();
let result = log_rollback(&state_dir, "prod", 0, "canary health check failed");
assert!(result.is_ok());
let log_path = state_dir.join("prod").join("events.jsonl");
let content = std::fs::read_to_string(&log_path).unwrap();
assert!(content.contains("rollback_triggered"));
assert!(content.contains("canary health check failed"));
assert!(content.contains("\"failed_step\":0"));
}
#[test]
fn multiple_events_append() {
let dir = tempfile::tempdir().unwrap();
let state_dir = dir.path().join("state");
std::fs::create_dir_all(&state_dir).unwrap();
let p1 = PromotionParams {
state_dir: &state_dir,
target_env: "staging",
source: "dev",
target: "staging",
gates_passed: 2,
gates_total: 2,
rollout_strategy: None,
};
log_promotion(&p1).unwrap();
let p2 = PromotionParams {
state_dir: &state_dir,
target_env: "staging",
source: "dev",
target: "staging",
gates_passed: 3,
gates_total: 3,
rollout_strategy: None,
};
log_promotion(&p2).unwrap();
let log_path = state_dir.join("staging").join("events.jsonl");
let content = std::fs::read_to_string(&log_path).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2, "expected 2 events appended");
}
#[test]
fn event_serialization_roundtrip() {
let event = ProvenanceEvent::PromotionCompleted {
source: "dev".into(),
target: "staging".into(),
success: true,
gates_passed: 4,
gates_total: 4,
rollout_strategy: Some("percentage".into()),
};
let json = serde_json::to_string(&event).unwrap();
let deserialized: ProvenanceEvent = serde_json::from_str(&json).unwrap();
match deserialized {
ProvenanceEvent::PromotionCompleted {
source,
target,
success,
gates_passed,
gates_total,
rollout_strategy,
} => {
assert_eq!(source, "dev");
assert_eq!(target, "staging");
assert!(success);
assert_eq!(gates_passed, 4);
assert_eq!(gates_total, 4);
assert_eq!(rollout_strategy.as_deref(), Some("percentage"));
}
_ => panic!("wrong variant"),
}
}
#[test]
fn rollback_event_serialization() {
let event = ProvenanceEvent::RollbackTriggered {
environment: "prod".into(),
failed_step: 2,
reason: "timeout".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("rollback_triggered"));
let deserialized: ProvenanceEvent = serde_json::from_str(&json).unwrap();
match deserialized {
ProvenanceEvent::RollbackTriggered {
environment,
failed_step,
reason,
} => {
assert_eq!(environment, "prod");
assert_eq!(failed_step, 2);
assert_eq!(reason, "timeout");
}
_ => panic!("wrong variant"),
}
}
}