1use crate::error::{ExecutionError, ExecutionResult};
8use crate::models::{ExecutionPlan, RiskLevel};
9use ricecoder_workflows::approval::{ApprovalGate, ApprovalRequest};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
14pub struct ApprovalSummary {
15 pub plan_id: String,
17 pub plan_name: String,
19 pub step_count: usize,
21 pub risk_level: RiskLevel,
23 pub risk_score: f32,
25 pub risk_factors: String,
27 pub estimated_duration_secs: u64,
29 pub requires_approval: bool,
31}
32
33pub struct ApprovalManager {
39 gate: ApprovalGate,
41 plan_requests: HashMap<String, String>,
43 request_plans: HashMap<String, String>,
45}
46
47impl Default for ApprovalManager {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl ApprovalManager {
54 pub fn new() -> Self {
56 ApprovalManager {
57 gate: ApprovalGate::new(),
58 plan_requests: HashMap::new(),
59 request_plans: HashMap::new(),
60 }
61 }
62
63 pub fn request_approval(&mut self, plan: &ExecutionPlan) -> ExecutionResult<String> {
68 let summary = ApprovalSummary::from_plan(plan);
69 let message = summary.format_message();
70
71 let request_id = self
72 .gate
73 .request_approval(plan.id.clone(), message, 1_800_000) .map_err(|e| {
75 ExecutionError::ValidationError(format!("Failed to request approval: {}", e))
76 })?;
77
78 self.plan_requests
79 .insert(plan.id.clone(), request_id.clone());
80 self.request_plans
81 .insert(request_id.clone(), plan.id.clone());
82
83 tracing::info!(
84 plan_id = %plan.id,
85 request_id = %request_id,
86 risk_level = ?plan.risk_score.level,
87 "Approval requested for plan"
88 );
89
90 Ok(request_id)
91 }
92
93 pub fn approve(&mut self, request_id: &str, comments: Option<String>) -> ExecutionResult<()> {
97 self.gate
98 .approve(request_id, comments.clone())
99 .map_err(|e| ExecutionError::ValidationError(format!("Failed to approve: {}", e)))?;
100
101 if let Some(plan_id) = self.request_plans.get(request_id) {
102 tracing::info!(
103 plan_id = %plan_id,
104 request_id = %request_id,
105 comments = ?comments,
106 "Plan approved"
107 );
108 }
109
110 Ok(())
111 }
112
113 pub fn reject(&mut self, request_id: &str, comments: Option<String>) -> ExecutionResult<()> {
117 self.gate
118 .reject(request_id, comments.clone())
119 .map_err(|e| ExecutionError::ValidationError(format!("Failed to reject: {}", e)))?;
120
121 if let Some(plan_id) = self.request_plans.get(request_id) {
122 tracing::info!(
123 plan_id = %plan_id,
124 request_id = %request_id,
125 comments = ?comments,
126 "Plan rejected"
127 );
128 }
129
130 Ok(())
131 }
132
133 pub fn is_approved(&self, request_id: &str) -> ExecutionResult<bool> {
137 self.gate.is_approved(request_id).map_err(|e| {
138 ExecutionError::ValidationError(format!("Failed to check approval status: {}", e))
139 })
140 }
141
142 pub fn is_rejected(&self, request_id: &str) -> ExecutionResult<bool> {
146 self.gate.is_rejected(request_id).map_err(|e| {
147 ExecutionError::ValidationError(format!("Failed to check rejection status: {}", e))
148 })
149 }
150
151 pub fn is_pending(&self, request_id: &str) -> ExecutionResult<bool> {
153 self.gate.is_pending(request_id).map_err(|e| {
154 ExecutionError::ValidationError(format!("Failed to check pending status: {}", e))
155 })
156 }
157
158 pub fn get_request(&self, request_id: &str) -> ExecutionResult<ApprovalRequest> {
160 self.gate.get_request_status(request_id).map_err(|e| {
161 ExecutionError::ValidationError(format!("Failed to get request status: {}", e))
162 })
163 }
164
165 pub fn get_pending_requests(&self) -> Vec<ApprovalRequest> {
167 self.gate.get_pending_requests()
168 }
169
170 pub fn get_request_id(&self, plan_id: &str) -> Option<String> {
172 self.plan_requests.get(plan_id).cloned()
173 }
174
175 pub fn approval_required(risk_level: RiskLevel) -> bool {
179 matches!(risk_level, RiskLevel::High | RiskLevel::Critical)
180 }
181
182 pub fn approval_strongly_recommended(risk_level: RiskLevel) -> bool {
186 matches!(risk_level, RiskLevel::Critical)
187 }
188}
189
190impl ApprovalSummary {
191 pub fn from_plan(plan: &ExecutionPlan) -> Self {
193 let risk_factors = plan
194 .risk_score
195 .factors
196 .iter()
197 .map(|f| format!("- {}: {}", f.name, f.description))
198 .collect::<Vec<_>>()
199 .join("\n");
200
201 ApprovalSummary {
202 plan_id: plan.id.clone(),
203 plan_name: plan.name.clone(),
204 step_count: plan.steps.len(),
205 risk_level: plan.risk_score.level,
206 risk_score: plan.risk_score.score,
207 risk_factors,
208 estimated_duration_secs: plan.estimated_duration.as_secs(),
209 requires_approval: plan.requires_approval,
210 }
211 }
212
213 pub fn format_message(&self) -> String {
215 format!(
216 "Plan: {}\nSteps: {}\nRisk Level: {:?}\nRisk Score: {:.2}\nEstimated Duration: {}s\n\nRisk Factors:\n{}",
217 self.plan_name,
218 self.step_count,
219 self.risk_level,
220 self.risk_score,
221 self.estimated_duration_secs,
222 self.risk_factors
223 )
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::models::{ExecutionStep, RiskFactor, RiskScore, StepAction};
231
232 fn create_test_plan() -> ExecutionPlan {
233 let step = ExecutionStep::new(
234 "Test step".to_string(),
235 StepAction::CreateFile {
236 path: "test.txt".to_string(),
237 content: "test".to_string(),
238 },
239 );
240
241 let mut plan = ExecutionPlan::new("Test Plan".to_string(), vec![step]);
242 plan.risk_score = RiskScore {
243 level: RiskLevel::High,
244 score: 1.8,
245 factors: vec![RiskFactor {
246 name: "file_count".to_string(),
247 weight: 0.5,
248 description: "1 file modified".to_string(),
249 }],
250 };
251 plan.requires_approval = true;
252
253 plan
254 }
255
256 #[test]
257 fn test_create_approval_manager() {
258 let manager = ApprovalManager::new();
259 assert_eq!(manager.plan_requests.len(), 0);
260 assert_eq!(manager.request_plans.len(), 0);
261 }
262
263 #[test]
264 fn test_request_approval() {
265 let mut manager = ApprovalManager::new();
266 let plan = create_test_plan();
267
268 let request_id = manager.request_approval(&plan).unwrap();
269 assert!(!request_id.is_empty());
270 assert_eq!(manager.plan_requests.len(), 1);
271 assert_eq!(manager.request_plans.len(), 1);
272 }
273
274 #[test]
275 fn test_approve_plan() {
276 let mut manager = ApprovalManager::new();
277 let plan = create_test_plan();
278
279 let request_id = manager.request_approval(&plan).unwrap();
280 manager
281 .approve(&request_id, Some("Looks good".to_string()))
282 .unwrap();
283
284 assert!(manager.is_approved(&request_id).unwrap());
285 assert!(!manager.is_rejected(&request_id).unwrap());
286 assert!(!manager.is_pending(&request_id).unwrap());
287 }
288
289 #[test]
290 fn test_reject_plan() {
291 let mut manager = ApprovalManager::new();
292 let plan = create_test_plan();
293
294 let request_id = manager.request_approval(&plan).unwrap();
295 manager
296 .reject(&request_id, Some("Needs changes".to_string()))
297 .unwrap();
298
299 assert!(!manager.is_approved(&request_id).unwrap());
300 assert!(manager.is_rejected(&request_id).unwrap());
301 assert!(!manager.is_pending(&request_id).unwrap());
302 }
303
304 #[test]
305 fn test_get_request_id() {
306 let mut manager = ApprovalManager::new();
307 let plan = create_test_plan();
308 let plan_id = plan.id.clone();
309
310 let request_id = manager.request_approval(&plan).unwrap();
311 assert_eq!(manager.get_request_id(&plan_id), Some(request_id));
312 }
313
314 #[test]
315 fn test_approval_required() {
316 assert!(!ApprovalManager::approval_required(RiskLevel::Low));
317 assert!(!ApprovalManager::approval_required(RiskLevel::Medium));
318 assert!(ApprovalManager::approval_required(RiskLevel::High));
319 assert!(ApprovalManager::approval_required(RiskLevel::Critical));
320 }
321
322 #[test]
323 fn test_approval_strongly_recommended() {
324 assert!(!ApprovalManager::approval_strongly_recommended(
325 RiskLevel::Low
326 ));
327 assert!(!ApprovalManager::approval_strongly_recommended(
328 RiskLevel::Medium
329 ));
330 assert!(!ApprovalManager::approval_strongly_recommended(
331 RiskLevel::High
332 ));
333 assert!(ApprovalManager::approval_strongly_recommended(
334 RiskLevel::Critical
335 ));
336 }
337
338 #[test]
339 fn test_approval_summary_from_plan() {
340 let plan = create_test_plan();
341 let summary = ApprovalSummary::from_plan(&plan);
342
343 assert_eq!(summary.plan_id, plan.id);
344 assert_eq!(summary.plan_name, "Test Plan");
345 assert_eq!(summary.step_count, 1);
346 assert_eq!(summary.risk_level, RiskLevel::High);
347 assert!(summary.requires_approval);
348 }
349
350 #[test]
351 fn test_approval_summary_format_message() {
352 let plan = create_test_plan();
353 let summary = ApprovalSummary::from_plan(&plan);
354 let message = summary.format_message();
355
356 assert!(message.contains("Test Plan"));
357 assert!(message.contains("Steps: 1"));
358 assert!(message.contains("High"));
359 assert!(message.contains("Risk Factors"));
360 }
361
362 #[test]
363 fn test_get_pending_requests() {
364 let mut manager = ApprovalManager::new();
365 let plan1 = create_test_plan();
366 let plan2 = create_test_plan();
367
368 let _req1 = manager.request_approval(&plan1).unwrap();
369 let _req2 = manager.request_approval(&plan2).unwrap();
370
371 let pending = manager.get_pending_requests();
372 assert_eq!(pending.len(), 2);
373 }
374}