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