ricecoder_github/managers/
code_review_agent.rs

1//! Code Review Agent - Provides automated code review for pull requests
2
3use crate::errors::Result;
4use crate::models::PullRequest;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tracing::{debug, info};
8
9/// Code quality issue severity
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum IssueSeverity {
13    /// Critical issue that must be fixed
14    Critical,
15    /// Warning that should be addressed
16    Warning,
17    /// Informational suggestion
18    Info,
19}
20
21/// Code quality issue
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CodeQualityIssue {
24    /// Issue severity
25    pub severity: IssueSeverity,
26    /// Issue title
27    pub title: String,
28    /// Issue description
29    pub description: String,
30    /// File path where issue was found
31    pub file_path: String,
32    /// Line number (if applicable)
33    pub line_number: Option<u32>,
34    /// Suggested fix
35    pub suggested_fix: Option<String>,
36}
37
38impl CodeQualityIssue {
39    /// Create a new code quality issue
40    pub fn new(
41        severity: IssueSeverity,
42        title: impl Into<String>,
43        description: impl Into<String>,
44        file_path: impl Into<String>,
45    ) -> Self {
46        Self {
47            severity,
48            title: title.into(),
49            description: description.into(),
50            file_path: file_path.into(),
51            line_number: None,
52            suggested_fix: None,
53        }
54    }
55
56    /// Set line number
57    pub fn with_line_number(mut self, line: u32) -> Self {
58        self.line_number = Some(line);
59        self
60    }
61
62    /// Set suggested fix
63    pub fn with_suggested_fix(mut self, fix: impl Into<String>) -> Self {
64        self.suggested_fix = Some(fix.into());
65        self
66    }
67}
68
69/// Code review suggestion
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct CodeReviewSuggestion {
72    /// Suggestion title
73    pub title: String,
74    /// Suggestion body
75    pub body: String,
76    /// File path
77    pub file_path: String,
78    /// Line number (if applicable)
79    pub line_number: Option<u32>,
80    /// Is critical
81    pub is_critical: bool,
82}
83
84impl CodeReviewSuggestion {
85    /// Create a new suggestion
86    pub fn new(
87        title: impl Into<String>,
88        body: impl Into<String>,
89        file_path: impl Into<String>,
90    ) -> Self {
91        Self {
92            title: title.into(),
93            body: body.into(),
94            file_path: file_path.into(),
95            line_number: None,
96            is_critical: false,
97        }
98    }
99
100    /// Set line number
101    pub fn with_line_number(mut self, line: u32) -> Self {
102        self.line_number = Some(line);
103        self
104    }
105
106    /// Mark as critical
107    pub fn as_critical(mut self) -> Self {
108        self.is_critical = true;
109        self
110    }
111}
112
113/// Code review result
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct CodeReviewResult {
116    /// PR number
117    pub pr_number: u32,
118    /// Quality issues found
119    pub issues: Vec<CodeQualityIssue>,
120    /// Review suggestions
121    pub suggestions: Vec<CodeReviewSuggestion>,
122    /// Overall quality score (0-100)
123    pub quality_score: u32,
124    /// Is approved
125    pub approved: bool,
126    /// Approval reason
127    pub approval_reason: Option<String>,
128}
129
130impl CodeReviewResult {
131    /// Create a new code review result
132    pub fn new(pr_number: u32) -> Self {
133        Self {
134            pr_number,
135            issues: Vec::new(),
136            suggestions: Vec::new(),
137            quality_score: 100,
138            approved: true,
139            approval_reason: None,
140        }
141    }
142
143    /// Add an issue
144    pub fn with_issue(mut self, issue: CodeQualityIssue) -> Self {
145        // Reduce quality score based on severity
146        match issue.severity {
147            IssueSeverity::Critical => self.quality_score = self.quality_score.saturating_sub(20),
148            IssueSeverity::Warning => self.quality_score = self.quality_score.saturating_sub(10),
149            IssueSeverity::Info => self.quality_score = self.quality_score.saturating_sub(5),
150        }
151        self.issues.push(issue);
152        self
153    }
154
155    /// Add issues
156    pub fn with_issues(mut self, issues: Vec<CodeQualityIssue>) -> Self {
157        for issue in issues {
158            self = self.with_issue(issue);
159        }
160        self
161    }
162
163    /// Add a suggestion
164    pub fn with_suggestion(mut self, suggestion: CodeReviewSuggestion) -> Self {
165        self.suggestions.push(suggestion);
166        self
167    }
168
169    /// Add suggestions
170    pub fn with_suggestions(mut self, suggestions: Vec<CodeReviewSuggestion>) -> Self {
171        for suggestion in suggestions {
172            self = self.with_suggestion(suggestion);
173        }
174        self
175    }
176
177    /// Set approval status
178    pub fn set_approved(mut self, approved: bool, reason: Option<String>) -> Self {
179        self.approved = approved;
180        self.approval_reason = reason;
181        self
182    }
183
184    /// Check if has critical issues
185    pub fn has_critical_issues(&self) -> bool {
186        self.issues.iter().any(|i| i.severity == IssueSeverity::Critical)
187    }
188
189    /// Get critical issues count
190    pub fn critical_issues_count(&self) -> usize {
191        self.issues.iter().filter(|i| i.severity == IssueSeverity::Critical).count()
192    }
193
194    /// Get warnings count
195    pub fn warnings_count(&self) -> usize {
196        self.issues.iter().filter(|i| i.severity == IssueSeverity::Warning).count()
197    }
198
199    /// Get info count
200    pub fn info_count(&self) -> usize {
201        self.issues.iter().filter(|i| i.severity == IssueSeverity::Info).count()
202    }
203}
204
205/// Code review standards configuration
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct CodeReviewStandards {
208    /// Minimum quality score for approval (0-100)
209    pub min_quality_score: u32,
210    /// Require all critical issues to be fixed
211    pub require_critical_fixes: bool,
212    /// Require all warnings to be addressed
213    pub require_warning_fixes: bool,
214    /// Custom standards rules
215    pub custom_rules: HashMap<String, String>,
216}
217
218impl Default for CodeReviewStandards {
219    fn default() -> Self {
220        Self {
221            min_quality_score: 70,
222            require_critical_fixes: true,
223            require_warning_fixes: false,
224            custom_rules: HashMap::new(),
225        }
226    }
227}
228
229impl CodeReviewStandards {
230    /// Create new standards
231    pub fn new(min_quality_score: u32) -> Self {
232        Self {
233            min_quality_score,
234            ..Default::default()
235        }
236    }
237
238    /// Set require critical fixes
239    pub fn require_critical_fixes(mut self, require: bool) -> Self {
240        self.require_critical_fixes = require;
241        self
242    }
243
244    /// Set require warning fixes
245    pub fn require_warning_fixes(mut self, require: bool) -> Self {
246        self.require_warning_fixes = require;
247        self
248    }
249
250    /// Add custom rule
251    pub fn with_rule(mut self, name: impl Into<String>, rule: impl Into<String>) -> Self {
252        self.custom_rules.insert(name.into(), rule.into());
253        self
254    }
255}
256
257/// Code Review Agent - Provides automated code review
258pub struct CodeReviewAgent {
259    /// Code review standards
260    pub standards: CodeReviewStandards,
261}
262
263impl CodeReviewAgent {
264    /// Create a new code review agent
265    pub fn new() -> Self {
266        Self {
267            standards: CodeReviewStandards::default(),
268        }
269    }
270
271    /// Create with custom standards
272    pub fn with_standards(standards: CodeReviewStandards) -> Self {
273        Self { standards }
274    }
275
276    /// Analyze PR code for quality issues
277    pub fn analyze_code(&self, pr: &PullRequest) -> Result<Vec<CodeQualityIssue>> {
278        debug!(
279            pr_number = pr.number,
280            file_count = pr.files.len(),
281            "Analyzing PR code for quality issues"
282        );
283
284        let mut issues = Vec::new();
285
286        // Analyze each file
287        for file in &pr.files {
288            // Check for large files
289            if file.additions + file.deletions > 500 {
290                issues.push(
291                    CodeQualityIssue::new(
292                        IssueSeverity::Warning,
293                        "Large file change",
294                        format!(
295                            "File {} has {} lines changed, consider breaking into smaller changes",
296                            file.path,
297                            file.additions + file.deletions
298                        ),
299                        &file.path,
300                    )
301                );
302            }
303
304            // Check for excessive deletions
305            if file.deletions > file.additions * 2 {
306                issues.push(
307                    CodeQualityIssue::new(
308                        IssueSeverity::Info,
309                        "Large deletion",
310                        format!(
311                            "File {} has significant deletions ({} lines)",
312                            file.path, file.deletions
313                        ),
314                        &file.path,
315                    )
316                );
317            }
318        }
319
320        // Check PR body for common issues
321        if pr.body.is_empty() {
322            issues.push(
323                CodeQualityIssue::new(
324                    IssueSeverity::Warning,
325                    "Missing PR description",
326                    "PR body is empty, please provide a description of changes",
327                    "PR",
328                )
329            );
330        }
331
332        // Check PR title length
333        if pr.title.len() > 100 {
334            issues.push(
335                CodeQualityIssue::new(
336                    IssueSeverity::Info,
337                    "Long PR title",
338                    format!("PR title is {} characters, consider shortening", pr.title.len()),
339                    "PR",
340                )
341            );
342        }
343
344        info!(
345            pr_number = pr.number,
346            issue_count = issues.len(),
347            "Code analysis complete"
348        );
349
350        Ok(issues)
351    }
352
353    /// Generate code review suggestions
354    pub fn generate_suggestions(&self, issues: &[CodeQualityIssue]) -> Result<Vec<CodeReviewSuggestion>> {
355        debug!(
356            issue_count = issues.len(),
357            "Generating code review suggestions"
358        );
359
360        let mut suggestions = Vec::new();
361
362        for issue in issues {
363            let suggestion = CodeReviewSuggestion::new(
364                &issue.title,
365                &issue.description,
366                &issue.file_path,
367            )
368            .with_line_number(issue.line_number.unwrap_or(0));
369
370            let suggestion = if issue.severity == IssueSeverity::Critical {
371                suggestion.as_critical()
372            } else {
373                suggestion
374            };
375
376            suggestions.push(suggestion);
377        }
378
379        info!(
380            suggestion_count = suggestions.len(),
381            "Suggestions generated"
382        );
383
384        Ok(suggestions)
385    }
386
387    /// Validate code against project standards
388    pub fn validate_standards(&self, pr: &PullRequest) -> Result<Vec<CodeQualityIssue>> {
389        debug!(
390            pr_number = pr.number,
391            "Validating code against project standards"
392        );
393
394        let mut issues = Vec::new();
395
396        // Check for minimum file count
397        if pr.files.is_empty() {
398            issues.push(
399                CodeQualityIssue::new(
400                    IssueSeverity::Critical,
401                    "No files changed",
402                    "PR has no file changes",
403                    "PR",
404                )
405            );
406        }
407
408        // Check for branch naming convention
409        if !self.is_valid_branch_name(&pr.branch) {
410            issues.push(
411                CodeQualityIssue::new(
412                    IssueSeverity::Warning,
413                    "Invalid branch name",
414                    format!(
415                        "Branch name '{}' does not follow naming conventions (use feature/, bugfix/, hotfix/)",
416                        pr.branch
417                    ),
418                    "PR",
419                )
420            );
421        }
422
423        // Apply custom rules
424        for rule_name in self.standards.custom_rules.keys() {
425            debug!(rule = rule_name, "Applying custom rule");
426        }
427
428        info!(
429            pr_number = pr.number,
430            issue_count = issues.len(),
431            "Standards validation complete"
432        );
433
434        Ok(issues)
435    }
436
437    /// Check if branch name is valid
438    pub fn is_valid_branch_name(&self, branch: &str) -> bool {
439        // Allow common branch naming patterns
440        branch.starts_with("feature/")
441            || branch.starts_with("bugfix/")
442            || branch.starts_with("hotfix/")
443            || branch.starts_with("release/")
444            || branch == "main"
445            || branch == "develop"
446            || branch == "master"
447    }
448
449    /// Generate code review summary
450    pub fn generate_summary(&self, result: &CodeReviewResult) -> Result<String> {
451        debug!(
452            pr_number = result.pr_number,
453            quality_score = result.quality_score,
454            "Generating code review summary"
455        );
456
457        let mut summary = format!(
458            "## Code Review Summary\n\n**PR #{}**\n\n**Quality Score: {}/100**\n\n",
459            result.pr_number, result.quality_score
460        );
461
462        // Add approval status
463        if result.approved {
464            summary.push_str("✅ **APPROVED**\n\n");
465            if let Some(reason) = &result.approval_reason {
466                summary.push_str(&format!("Reason: {}\n\n", reason));
467            }
468        } else {
469            summary.push_str("❌ **NEEDS REVIEW**\n\n");
470            if let Some(reason) = &result.approval_reason {
471                summary.push_str(&format!("Reason: {}\n\n", reason));
472            }
473        }
474
475        // Add issues summary
476        if !result.issues.is_empty() {
477            summary.push_str("### Issues Found\n\n");
478            summary.push_str(&format!(
479                "- **Critical**: {}\n",
480                result.critical_issues_count()
481            ));
482            summary.push_str(&format!("- **Warnings**: {}\n", result.warnings_count()));
483            summary.push_str(&format!("- **Info**: {}\n\n", result.info_count()));
484
485            // Add critical issues
486            let critical: Vec<_> = result
487                .issues
488                .iter()
489                .filter(|i| i.severity == IssueSeverity::Critical)
490                .collect();
491            if !critical.is_empty() {
492                summary.push_str("#### Critical Issues\n\n");
493                for issue in critical {
494                    summary.push_str(&format!(
495                        "- **{}** ({}): {}\n",
496                        issue.title, issue.file_path, issue.description
497                    ));
498                }
499                summary.push('\n');
500            }
501        }
502
503        // Add suggestions
504        if !result.suggestions.is_empty() {
505            summary.push_str("### Suggestions\n\n");
506            for suggestion in &result.suggestions {
507                summary.push_str(&format!(
508                    "- **{}** ({}): {}\n",
509                    suggestion.title, suggestion.file_path, suggestion.body
510                ));
511            }
512        }
513
514        info!(
515            pr_number = result.pr_number,
516            summary_length = summary.len(),
517            "Summary generated"
518        );
519
520        Ok(summary)
521    }
522
523    /// Determine if PR should be approved
524    pub fn should_approve(&self, result: &CodeReviewResult) -> Result<bool> {
525        debug!(
526            pr_number = result.pr_number,
527            quality_score = result.quality_score,
528            "Determining approval status"
529        );
530
531        // Check quality score
532        if result.quality_score < self.standards.min_quality_score {
533            return Ok(false);
534        }
535
536        // Check critical issues
537        if self.standards.require_critical_fixes && result.has_critical_issues() {
538            return Ok(false);
539        }
540
541        // Check warnings
542        if self.standards.require_warning_fixes && result.warnings_count() > 0 {
543            return Ok(false);
544        }
545
546        Ok(true)
547    }
548
549    /// Perform complete code review
550    pub fn review_pr(&self, pr: &PullRequest) -> Result<CodeReviewResult> {
551        debug!(pr_number = pr.number, "Starting complete code review");
552
553        // Analyze code
554        let code_issues = self.analyze_code(pr)?;
555
556        // Validate standards
557        let standard_issues = self.validate_standards(pr)?;
558
559        // Combine all issues
560        let mut all_issues = code_issues;
561        all_issues.extend(standard_issues);
562
563        // Generate suggestions
564        let suggestions = self.generate_suggestions(&all_issues)?;
565
566        // Create result
567        let mut result = CodeReviewResult::new(pr.number)
568            .with_issues(all_issues);
569
570        // Add suggestions
571        result = result.with_suggestions(suggestions);
572
573        // Determine approval
574        let should_approve = self.should_approve(&result)?;
575        let approval_reason = Some(format!(
576            "Code quality score is {} (minimum: {})",
577            result.quality_score, self.standards.min_quality_score
578        ));
579
580        result = result.set_approved(should_approve, approval_reason);
581
582        info!(
583            pr_number = pr.number,
584            approved = result.approved,
585            quality_score = result.quality_score,
586            "Code review complete"
587        );
588
589        Ok(result)
590    }
591}
592
593impl Default for CodeReviewAgent {
594    fn default() -> Self {
595        Self::new()
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602    use crate::models::{FileChange, PrStatus};
603
604    fn create_test_pr() -> PullRequest {
605        PullRequest {
606            id: 1,
607            number: 123,
608            title: "Test PR".to_string(),
609            body: "This is a test PR".to_string(),
610            branch: "feature/test".to_string(),
611            base: "main".to_string(),
612            status: PrStatus::Open,
613            files: vec![FileChange {
614                path: "src/main.rs".to_string(),
615                change_type: "modified".to_string(),
616                additions: 50,
617                deletions: 10,
618            }],
619            created_at: chrono::Utc::now(),
620            updated_at: chrono::Utc::now(),
621        }
622    }
623
624    #[test]
625    fn test_code_quality_issue_creation() {
626        let issue = CodeQualityIssue::new(
627            IssueSeverity::Warning,
628            "Test issue",
629            "This is a test",
630            "test.rs",
631        );
632        assert_eq!(issue.severity, IssueSeverity::Warning);
633        assert_eq!(issue.title, "Test issue");
634        assert_eq!(issue.file_path, "test.rs");
635    }
636
637    #[test]
638    fn test_code_quality_issue_with_line_number() {
639        let issue = CodeQualityIssue::new(
640            IssueSeverity::Critical,
641            "Test",
642            "Description",
643            "test.rs",
644        )
645        .with_line_number(42);
646        assert_eq!(issue.line_number, Some(42));
647    }
648
649    #[test]
650    fn test_code_review_suggestion_creation() {
651        let suggestion = CodeReviewSuggestion::new("Test", "Body", "test.rs");
652        assert_eq!(suggestion.title, "Test");
653        assert_eq!(suggestion.body, "Body");
654        assert!(!suggestion.is_critical);
655    }
656
657    #[test]
658    fn test_code_review_suggestion_as_critical() {
659        let suggestion = CodeReviewSuggestion::new("Test", "Body", "test.rs").as_critical();
660        assert!(suggestion.is_critical);
661    }
662
663    #[test]
664    fn test_code_review_result_creation() {
665        let result = CodeReviewResult::new(123);
666        assert_eq!(result.pr_number, 123);
667        assert_eq!(result.quality_score, 100);
668        assert!(result.approved);
669    }
670
671    #[test]
672    fn test_code_review_result_with_issue() {
673        let issue = CodeQualityIssue::new(
674            IssueSeverity::Critical,
675            "Test",
676            "Description",
677            "test.rs",
678        );
679        let result = CodeReviewResult::new(123).with_issue(issue);
680        assert_eq!(result.issues.len(), 1);
681        assert_eq!(result.critical_issues_count(), 1);
682        assert!(result.quality_score < 100);
683    }
684
685    #[test]
686    fn test_code_review_result_quality_score_calculation() {
687        let critical = CodeQualityIssue::new(
688            IssueSeverity::Critical,
689            "Critical",
690            "Description",
691            "test.rs",
692        );
693        let warning = CodeQualityIssue::new(
694            IssueSeverity::Warning,
695            "Warning",
696            "Description",
697            "test.rs",
698        );
699        let result = CodeReviewResult::new(123)
700            .with_issue(critical)
701            .with_issue(warning);
702        assert_eq!(result.quality_score, 70); // 100 - 20 - 10
703    }
704
705    #[test]
706    fn test_code_review_standards_default() {
707        let standards = CodeReviewStandards::default();
708        assert_eq!(standards.min_quality_score, 70);
709        assert!(standards.require_critical_fixes);
710        assert!(!standards.require_warning_fixes);
711    }
712
713    #[test]
714    fn test_code_review_agent_creation() {
715        let agent = CodeReviewAgent::new();
716        assert_eq!(agent.standards.min_quality_score, 70);
717    }
718
719    #[test]
720    fn test_analyze_code_empty_files() {
721        let agent = CodeReviewAgent::new();
722        let mut pr = create_test_pr();
723        pr.files.clear();
724        let issues = agent.analyze_code(&pr).unwrap();
725        assert!(issues.is_empty());
726    }
727
728    #[test]
729    fn test_analyze_code_large_file() {
730        let agent = CodeReviewAgent::new();
731        let mut pr = create_test_pr();
732        pr.files[0].additions = 300;
733        pr.files[0].deletions = 300;
734        let issues = agent.analyze_code(&pr).unwrap();
735        assert!(!issues.is_empty());
736        assert!(issues.iter().any(|i| i.title.contains("Large file")));
737    }
738
739    #[test]
740    fn test_analyze_code_empty_body() {
741        let agent = CodeReviewAgent::new();
742        let mut pr = create_test_pr();
743        pr.body.clear();
744        let issues = agent.analyze_code(&pr).unwrap();
745        assert!(issues.iter().any(|i| i.title.contains("Missing PR description")));
746    }
747
748    #[test]
749    fn test_validate_standards_no_files() {
750        let agent = CodeReviewAgent::new();
751        let mut pr = create_test_pr();
752        pr.files.clear();
753        let issues = agent.validate_standards(&pr).unwrap();
754        assert!(issues.iter().any(|i| i.severity == IssueSeverity::Critical));
755    }
756
757    #[test]
758    fn test_validate_standards_invalid_branch() {
759        let agent = CodeReviewAgent::new();
760        let mut pr = create_test_pr();
761        pr.branch = "invalid-branch".to_string();
762        let issues = agent.validate_standards(&pr).unwrap();
763        assert!(issues.iter().any(|i| i.title.contains("Invalid branch")));
764    }
765
766    #[test]
767    fn test_is_valid_branch_name() {
768        let agent = CodeReviewAgent::new();
769        assert!(agent.is_valid_branch_name("feature/test"));
770        assert!(agent.is_valid_branch_name("bugfix/test"));
771        assert!(agent.is_valid_branch_name("hotfix/test"));
772        assert!(agent.is_valid_branch_name("main"));
773        assert!(!agent.is_valid_branch_name("invalid"));
774    }
775
776    #[test]
777    fn test_generate_suggestions() {
778        let agent = CodeReviewAgent::new();
779        let issues = vec![CodeQualityIssue::new(
780            IssueSeverity::Warning,
781            "Test",
782            "Description",
783            "test.rs",
784        )];
785        let suggestions = agent.generate_suggestions(&issues).unwrap();
786        assert_eq!(suggestions.len(), 1);
787        assert_eq!(suggestions[0].title, "Test");
788    }
789
790    #[test]
791    fn test_generate_summary() {
792        let agent = CodeReviewAgent::new();
793        let result = CodeReviewResult::new(123);
794        let summary = agent.generate_summary(&result).unwrap();
795        assert!(summary.contains("Code Review Summary"));
796        assert!(summary.contains("PR #123"));
797        assert!(summary.contains("APPROVED"));
798    }
799
800    #[test]
801    fn test_should_approve_high_quality() {
802        let agent = CodeReviewAgent::new();
803        let result = CodeReviewResult::new(123).set_approved(true, None);
804        assert!(agent.should_approve(&result).unwrap());
805    }
806
807    #[test]
808    fn test_should_approve_low_quality() {
809        let agent = CodeReviewAgent::new();
810        let mut result = CodeReviewResult::new(123);
811        result.quality_score = 50;
812        assert!(!agent.should_approve(&result).unwrap());
813    }
814
815    #[test]
816    fn test_should_approve_with_critical_issues() {
817        let standards = CodeReviewStandards::default().require_critical_fixes(true);
818        let agent = CodeReviewAgent::with_standards(standards);
819        let issue = CodeQualityIssue::new(
820            IssueSeverity::Critical,
821            "Critical",
822            "Description",
823            "test.rs",
824        );
825        let result = CodeReviewResult::new(123).with_issue(issue);
826        assert!(!agent.should_approve(&result).unwrap());
827    }
828
829    #[test]
830    fn test_review_pr_complete() {
831        let agent = CodeReviewAgent::new();
832        let pr = create_test_pr();
833        let result = agent.review_pr(&pr).unwrap();
834        assert_eq!(result.pr_number, 123);
835        assert!(result.quality_score > 0);
836    }
837
838    #[test]
839    fn test_review_pr_with_issues() {
840        let agent = CodeReviewAgent::new();
841        let mut pr = create_test_pr();
842        pr.body.clear();
843        pr.branch = "invalid".to_string();
844        let result = agent.review_pr(&pr).unwrap();
845        assert!(!result.issues.is_empty());
846    }
847}