dk_runner/steps/human_approve/
mod.rs1pub 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); pub 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 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
93pub 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}