Skip to main content

dk_runner/steps/agent_review/
mod.rs

1pub mod provider;
2pub mod claude;
3pub mod prompt;
4pub mod parse;
5
6use std::time::Instant;
7use crate::executor::{StepOutput, StepStatus};
8use crate::findings::{Finding, Suggestion};
9use provider::{ReviewProvider, ReviewRequest, FileContext, ReviewVerdict};
10
11pub async fn run_agent_review_step_with_provider(
12    provider: &dyn ReviewProvider,
13    diff: &str,
14    files: Vec<FileContext>,
15    intent: &str,
16) -> (StepOutput, Vec<Finding>, Vec<Suggestion>) {
17    let start = Instant::now();
18    let request = ReviewRequest {
19        diff: diff.to_string(),
20        context: files,
21        language: "rust".to_string(),
22        intent: intent.to_string(),
23    };
24
25    match provider.review(request).await {
26        Ok(response) => {
27            let status = match response.verdict {
28                ReviewVerdict::Approve => StepStatus::Pass,
29                ReviewVerdict::RequestChanges => StepStatus::Fail,
30                ReviewVerdict::Comment => StepStatus::Pass,
31            };
32            (
33                StepOutput {
34                    status,
35                    stdout: format!(
36                        "Agent Review ({}): {}",
37                        provider.name(),
38                        response.summary
39                    ),
40                    stderr: String::new(),
41                    duration: start.elapsed(),
42                },
43                response.findings,
44                response.suggestions,
45            )
46        }
47        Err(e) => {
48            let finding = Finding {
49                severity: crate::findings::Severity::Warning,
50                check_name: "agent-review-error".to_string(),
51                message: format!("Agent review failed: {e}"),
52                file_path: None,
53                line: None,
54                symbol: None,
55            };
56            (
57                StepOutput {
58                    status: StepStatus::Pass,
59                    stdout: format!(
60                        "Agent Review ({}): error -- {e}",
61                        provider.name()
62                    ),
63                    stderr: String::new(),
64                    duration: start.elapsed(),
65                },
66                vec![finding],
67                Vec::new(),
68            )
69        }
70    }
71}
72
73/// Legacy stub for when no provider is configured.
74pub async fn run_agent_review_step(prompt: &str) -> StepOutput {
75    let start = Instant::now();
76    StepOutput {
77        status: StepStatus::Pass,
78        stdout: format!(
79            "agent review: skipped (no provider configured)\nprompt: {}",
80            prompt
81        ),
82        stderr: String::new(),
83        duration: start.elapsed(),
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::findings::Severity;
91
92    struct MockProvider {
93        response: Result<provider::ReviewResponse, String>,
94    }
95
96    #[async_trait::async_trait]
97    impl provider::ReviewProvider for MockProvider {
98        fn name(&self) -> &str {
99            "mock"
100        }
101        async fn review(
102            &self,
103            _req: provider::ReviewRequest,
104        ) -> anyhow::Result<provider::ReviewResponse> {
105            match &self.response {
106                Ok(r) => Ok(r.clone()),
107                Err(msg) => anyhow::bail!("{}", msg),
108            }
109        }
110    }
111
112    fn make_request_args() -> (String, Vec<provider::FileContext>, String) {
113        ("diff".to_string(), vec![], "test intent".to_string())
114    }
115
116    #[tokio::test]
117    async fn test_approve_verdict_returns_pass() {
118        let provider = MockProvider {
119            response: Ok(provider::ReviewResponse {
120                summary: "LGTM".to_string(),
121                findings: vec![],
122                suggestions: vec![],
123                verdict: provider::ReviewVerdict::Approve,
124            }),
125        };
126        let (diff, files, intent) = make_request_args();
127        let (output, findings, suggestions) =
128            run_agent_review_step_with_provider(&provider, &diff, files, &intent).await;
129        assert_eq!(output.status, StepStatus::Pass);
130        assert!(findings.is_empty());
131        assert!(suggestions.is_empty());
132    }
133
134    #[tokio::test]
135    async fn test_request_changes_verdict_returns_fail() {
136        let provider = MockProvider {
137            response: Ok(provider::ReviewResponse {
138                summary: "Issues found".to_string(),
139                findings: vec![Finding {
140                    severity: Severity::Error,
141                    check_name: "test".to_string(),
142                    message: "bad".to_string(),
143                    file_path: None,
144                    line: None,
145                    symbol: None,
146                }],
147                suggestions: vec![],
148                verdict: provider::ReviewVerdict::RequestChanges,
149            }),
150        };
151        let (diff, files, intent) = make_request_args();
152        let (output, findings, _) =
153            run_agent_review_step_with_provider(&provider, &diff, files, &intent).await;
154        assert_eq!(output.status, StepStatus::Fail);
155        assert_eq!(findings.len(), 1);
156    }
157
158    #[tokio::test]
159    async fn test_provider_error_returns_pass_with_warning() {
160        let provider = MockProvider {
161            response: Err("API error".to_string()),
162        };
163        let (diff, files, intent) = make_request_args();
164        let (output, findings, _) =
165            run_agent_review_step_with_provider(&provider, &diff, files, &intent).await;
166        assert_eq!(output.status, StepStatus::Pass); // soft fail
167        assert_eq!(findings.len(), 1);
168        assert_eq!(findings[0].severity, Severity::Warning);
169    }
170
171    #[tokio::test]
172    async fn test_legacy_stub_passes() {
173        let output = run_agent_review_step("test prompt").await;
174        assert_eq!(output.status, StepStatus::Pass);
175        assert!(output.stdout.contains("no provider configured"));
176    }
177}