Skip to main content

dk_runner/steps/human_approve/
mod.rs

1pub mod types;
2
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5use uuid::Uuid;
6use tracing::info;
7
8use dk_engine::repo::Engine;
9use crate::executor::{StepOutput, StepStatus};
10use crate::findings::{Finding, Severity};
11
12const POLL_INTERVAL: Duration = Duration::from_secs(2);
13const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30 * 60); // 30 minutes
14
15/// Run human approval gate with DB polling.
16pub async fn run_human_approve_step_with_engine(
17    engine: &Arc<Engine>,
18    changeset_id: Uuid,
19    timeout: Option<Duration>,
20) -> (StepOutput, Vec<Finding>) {
21    let start = Instant::now();
22    let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT);
23
24    // Set changeset to awaiting_approval using optimistic locking:
25    // only transition if the changeset is currently in 'draft' state.
26    if let Err(e) = engine.changeset_store().update_status_if(changeset_id, "awaiting_approval", &["draft"]).await {
27        return (
28            StepOutput {
29                status: StepStatus::Fail,
30                stdout: format!("Failed to set awaiting_approval: {e}"),
31                stderr: String::new(),
32                duration: start.elapsed(),
33            },
34            Vec::new(),
35        );
36    }
37
38    info!(changeset_id = %changeset_id, "awaiting human approval (timeout: {:?})", timeout);
39
40    loop {
41        tokio::time::sleep(POLL_INTERVAL).await;
42
43        if start.elapsed() > timeout {
44            let finding = Finding {
45                severity: Severity::Warning,
46                check_name: "human-approval-timeout".to_string(),
47                message: format!("Human approval timed out after {:?}", timeout),
48                file_path: None, line: None, symbol: None,
49            };
50            return (
51                StepOutput {
52                    status: StepStatus::Fail,
53                    stdout: "Human approval: timed out".to_string(),
54                    stderr: String::new(),
55                    duration: start.elapsed(),
56                },
57                vec![finding],
58            );
59        }
60
61        match engine.changeset_store().get(changeset_id).await {
62            Ok(changeset) => match changeset.state.as_str() {
63                "approved" => return (
64                    StepOutput { status: StepStatus::Pass, stdout: "Human approval: approved".to_string(), stderr: String::new(), duration: start.elapsed() },
65                    Vec::new(),
66                ),
67                "rejected" => {
68                    let finding = Finding {
69                        severity: Severity::Error,
70                        check_name: "human-approval-rejected".to_string(),
71                        message: "Human reviewer rejected the changeset".to_string(),
72                        file_path: None, line: None, symbol: None,
73                    };
74                    return (
75                        StepOutput { status: StepStatus::Fail, stdout: "Human approval: rejected".to_string(), stderr: String::new(), duration: start.elapsed() },
76                        vec![finding],
77                    );
78                }
79                "awaiting_approval" => continue,
80                other => return (
81                    StepOutput { status: StepStatus::Skip, stdout: format!("Human approval: changeset moved to unexpected state '{other}'"), stderr: String::new(), duration: start.elapsed() },
82                    Vec::new(),
83                ),
84            },
85            Err(e) => return (
86                StepOutput { status: StepStatus::Fail, stdout: format!("Human approval: DB error — {e}"), stderr: String::new(), duration: start.elapsed() },
87                Vec::new(),
88            ),
89        }
90    }
91}
92
93/// Legacy stub for when no engine is available.
94pub async fn run_human_approve_step() -> StepOutput {
95    let start = Instant::now();
96    StepOutput {
97        status: StepStatus::Pass,
98        stdout: "human approval: auto-approved (not yet implemented)".to_string(),
99        stderr: String::new(),
100        duration: start.elapsed(),
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[tokio::test]
109    async fn test_legacy_stub_auto_approves() {
110        let output = run_human_approve_step().await;
111        assert_eq!(output.status, StepStatus::Pass);
112        assert!(output.stdout.contains("auto-approved"));
113    }
114}