use crate::error::{ExecutionError, ExecutionResult};
use crate::models::{ExecutionPlan, RiskLevel};
use ricecoder_workflows::approval::{ApprovalGate, ApprovalRequest};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ApprovalSummary {
pub plan_id: String,
pub plan_name: String,
pub step_count: usize,
pub risk_level: RiskLevel,
pub risk_score: f32,
pub risk_factors: String,
pub estimated_duration_secs: u64,
pub requires_approval: bool,
}
pub struct ApprovalManager {
gate: ApprovalGate,
plan_requests: HashMap<String, String>,
request_plans: HashMap<String, String>,
}
impl Default for ApprovalManager {
fn default() -> Self {
Self::new()
}
}
impl ApprovalManager {
pub fn new() -> Self {
ApprovalManager {
gate: ApprovalGate::new(),
plan_requests: HashMap::new(),
request_plans: HashMap::new(),
}
}
pub fn request_approval(&mut self, plan: &ExecutionPlan) -> ExecutionResult<String> {
let summary = ApprovalSummary::from_plan(plan);
let message = summary.format_message();
let request_id = self
.gate
.request_approval(plan.id.clone(), message, 1_800_000) .map_err(|e| {
ExecutionError::ValidationError(format!("Failed to request approval: {}", e))
})?;
self.plan_requests
.insert(plan.id.clone(), request_id.clone());
self.request_plans
.insert(request_id.clone(), plan.id.clone());
tracing::info!(
plan_id = %plan.id,
request_id = %request_id,
risk_level = ?plan.risk_score.level,
"Approval requested for plan"
);
Ok(request_id)
}
pub fn approve(&mut self, request_id: &str, comments: Option<String>) -> ExecutionResult<()> {
self.gate
.approve(request_id, comments.clone())
.map_err(|e| ExecutionError::ValidationError(format!("Failed to approve: {}", e)))?;
if let Some(plan_id) = self.request_plans.get(request_id) {
tracing::info!(
plan_id = %plan_id,
request_id = %request_id,
comments = ?comments,
"Plan approved"
);
}
Ok(())
}
pub fn reject(&mut self, request_id: &str, comments: Option<String>) -> ExecutionResult<()> {
self.gate
.reject(request_id, comments.clone())
.map_err(|e| ExecutionError::ValidationError(format!("Failed to reject: {}", e)))?;
if let Some(plan_id) = self.request_plans.get(request_id) {
tracing::info!(
plan_id = %plan_id,
request_id = %request_id,
comments = ?comments,
"Plan rejected"
);
}
Ok(())
}
pub fn is_approved(&self, request_id: &str) -> ExecutionResult<bool> {
self.gate.is_approved(request_id).map_err(|e| {
ExecutionError::ValidationError(format!("Failed to check approval status: {}", e))
})
}
pub fn is_rejected(&self, request_id: &str) -> ExecutionResult<bool> {
self.gate.is_rejected(request_id).map_err(|e| {
ExecutionError::ValidationError(format!("Failed to check rejection status: {}", e))
})
}
pub fn is_pending(&self, request_id: &str) -> ExecutionResult<bool> {
self.gate.is_pending(request_id).map_err(|e| {
ExecutionError::ValidationError(format!("Failed to check pending status: {}", e))
})
}
pub fn get_request(&self, request_id: &str) -> ExecutionResult<ApprovalRequest> {
self.gate.get_request_status(request_id).map_err(|e| {
ExecutionError::ValidationError(format!("Failed to get request status: {}", e))
})
}
pub fn get_pending_requests(&self) -> Vec<ApprovalRequest> {
self.gate.get_pending_requests()
}
pub fn get_request_id(&self, plan_id: &str) -> Option<String> {
self.plan_requests.get(plan_id).cloned()
}
pub fn approval_required(risk_level: RiskLevel) -> bool {
matches!(risk_level, RiskLevel::High | RiskLevel::Critical)
}
pub fn approval_strongly_recommended(risk_level: RiskLevel) -> bool {
matches!(risk_level, RiskLevel::Critical)
}
}
impl ApprovalSummary {
pub fn from_plan(plan: &ExecutionPlan) -> Self {
let risk_factors = plan
.risk_score
.factors
.iter()
.map(|f| format!("- {}: {}", f.name, f.description))
.collect::<Vec<_>>()
.join("\n");
ApprovalSummary {
plan_id: plan.id.clone(),
plan_name: plan.name.clone(),
step_count: plan.steps.len(),
risk_level: plan.risk_score.level,
risk_score: plan.risk_score.score,
risk_factors,
estimated_duration_secs: plan.estimated_duration.as_secs(),
requires_approval: plan.requires_approval,
}
}
pub fn format_message(&self) -> String {
format!(
"Plan: {}\nSteps: {}\nRisk Level: {:?}\nRisk Score: {:.2}\nEstimated Duration: {}s\n\nRisk Factors:\n{}",
self.plan_name,
self.step_count,
self.risk_level,
self.risk_score,
self.estimated_duration_secs,
self.risk_factors
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{ExecutionStep, RiskFactor, RiskScore, StepAction};
fn create_test_plan() -> ExecutionPlan {
let step = ExecutionStep::new(
"Test step".to_string(),
StepAction::CreateFile {
path: "test.txt".to_string(),
content: "test".to_string(),
},
);
let mut plan = ExecutionPlan::new("Test Plan".to_string(), vec![step]);
plan.risk_score = RiskScore {
level: RiskLevel::High,
score: 1.8,
factors: vec![RiskFactor {
name: "file_count".to_string(),
weight: 0.5,
description: "1 file modified".to_string(),
}],
};
plan.requires_approval = true;
plan
}
#[test]
fn test_create_approval_manager() {
let manager = ApprovalManager::new();
assert_eq!(manager.plan_requests.len(), 0);
assert_eq!(manager.request_plans.len(), 0);
}
#[test]
fn test_request_approval() {
let mut manager = ApprovalManager::new();
let plan = create_test_plan();
let request_id = manager.request_approval(&plan).unwrap();
assert!(!request_id.is_empty());
assert_eq!(manager.plan_requests.len(), 1);
assert_eq!(manager.request_plans.len(), 1);
}
#[test]
fn test_approve_plan() {
let mut manager = ApprovalManager::new();
let plan = create_test_plan();
let request_id = manager.request_approval(&plan).unwrap();
manager
.approve(&request_id, Some("Looks good".to_string()))
.unwrap();
assert!(manager.is_approved(&request_id).unwrap());
assert!(!manager.is_rejected(&request_id).unwrap());
assert!(!manager.is_pending(&request_id).unwrap());
}
#[test]
fn test_reject_plan() {
let mut manager = ApprovalManager::new();
let plan = create_test_plan();
let request_id = manager.request_approval(&plan).unwrap();
manager
.reject(&request_id, Some("Needs changes".to_string()))
.unwrap();
assert!(!manager.is_approved(&request_id).unwrap());
assert!(manager.is_rejected(&request_id).unwrap());
assert!(!manager.is_pending(&request_id).unwrap());
}
#[test]
fn test_get_request_id() {
let mut manager = ApprovalManager::new();
let plan = create_test_plan();
let plan_id = plan.id.clone();
let request_id = manager.request_approval(&plan).unwrap();
assert_eq!(manager.get_request_id(&plan_id), Some(request_id));
}
#[test]
fn test_approval_required() {
assert!(!ApprovalManager::approval_required(RiskLevel::Low));
assert!(!ApprovalManager::approval_required(RiskLevel::Medium));
assert!(ApprovalManager::approval_required(RiskLevel::High));
assert!(ApprovalManager::approval_required(RiskLevel::Critical));
}
#[test]
fn test_approval_strongly_recommended() {
assert!(!ApprovalManager::approval_strongly_recommended(
RiskLevel::Low
));
assert!(!ApprovalManager::approval_strongly_recommended(
RiskLevel::Medium
));
assert!(!ApprovalManager::approval_strongly_recommended(
RiskLevel::High
));
assert!(ApprovalManager::approval_strongly_recommended(
RiskLevel::Critical
));
}
#[test]
fn test_approval_summary_from_plan() {
let plan = create_test_plan();
let summary = ApprovalSummary::from_plan(&plan);
assert_eq!(summary.plan_id, plan.id);
assert_eq!(summary.plan_name, "Test Plan");
assert_eq!(summary.step_count, 1);
assert_eq!(summary.risk_level, RiskLevel::High);
assert!(summary.requires_approval);
}
#[test]
fn test_approval_summary_format_message() {
let plan = create_test_plan();
let summary = ApprovalSummary::from_plan(&plan);
let message = summary.format_message();
assert!(message.contains("Test Plan"));
assert!(message.contains("Steps: 1"));
assert!(message.contains("High"));
assert!(message.contains("Risk Factors"));
}
#[test]
fn test_get_pending_requests() {
let mut manager = ApprovalManager::new();
let plan1 = create_test_plan();
let plan2 = create_test_plan();
let _req1 = manager.request_approval(&plan1).unwrap();
let _req2 = manager.request_approval(&plan2).unwrap();
let pending = manager.get_pending_requests();
assert_eq!(pending.len(), 2);
}
}