Skip to main content

elizaos_plugin_github/actions/
review_pull_request.rs

1#![allow(missing_docs)]
2
3use async_trait::async_trait;
4use serde_json::json;
5
6use super::{ActionContext, ActionResult, GitHubAction};
7use crate::error::Result;
8use crate::types::{CreateReviewParams, ReviewEvent, ReviewState};
9use crate::GitHubService;
10
11pub struct ReviewPullRequestAction;
12
13impl ReviewPullRequestAction {
14    fn determine_review_event(text: &str) -> ReviewEvent {
15        let lower = text.to_lowercase();
16
17        if lower.contains("approve") || lower.contains("lgtm") || lower.contains("looks good") {
18            ReviewEvent::Approve
19        } else if lower.contains("request changes")
20            || lower.contains("needs work")
21            || lower.contains("fix")
22        {
23            ReviewEvent::RequestChanges
24        } else {
25            ReviewEvent::Comment
26        }
27    }
28}
29
30#[async_trait]
31impl GitHubAction for ReviewPullRequestAction {
32    fn name(&self) -> &str {
33        "REVIEW_GITHUB_PULL_REQUEST"
34    }
35
36    fn description(&self) -> &str {
37        "Creates a review on a GitHub pull request. Can approve, request changes, or add comments."
38    }
39
40    fn similes(&self) -> Vec<&str> {
41        vec![
42            "APPROVE_PR",
43            "REQUEST_CHANGES",
44            "COMMENT_ON_PR",
45            "REVIEW_PR",
46            "PR_REVIEW",
47            "CODE_REVIEW",
48        ]
49    }
50
51    async fn validate(&self, context: &ActionContext) -> Result<bool> {
52        let text = context
53            .message
54            .get("content")
55            .and_then(|c| c.get("text"))
56            .and_then(|t| t.as_str())
57            .unwrap_or("")
58            .to_lowercase();
59
60        Ok(text.contains("review")
61            || text.contains("approve")
62            || text.contains("request changes")
63            || text.contains("lgtm"))
64    }
65
66    async fn handler(
67        &self,
68        context: &ActionContext,
69        service: &GitHubService,
70    ) -> Result<ActionResult> {
71        let text = context
72            .message
73            .get("content")
74            .and_then(|c| c.get("text"))
75            .and_then(|t| t.as_str())
76            .unwrap_or("");
77
78        let pull_number = context
79            .state
80            .get("pullNumber")
81            .and_then(|p| p.as_u64())
82            .unwrap_or(0);
83
84        if pull_number == 0 {
85            return Ok(ActionResult::error("Pull request number is required"));
86        }
87
88        // Get review body from state or use text
89        let body = context
90            .state
91            .get("body")
92            .and_then(|b| b.as_str())
93            .map(|s| s.to_string())
94            .unwrap_or_else(|| text.to_string());
95
96        let event = context
97            .state
98            .get("event")
99            .and_then(|e| e.as_str())
100            .map(|e| match e.to_uppercase().as_str() {
101                "APPROVE" => ReviewEvent::Approve,
102                "REQUEST_CHANGES" => ReviewEvent::RequestChanges,
103                _ => ReviewEvent::Comment,
104            })
105            .unwrap_or_else(|| Self::determine_review_event(text));
106
107        let params = CreateReviewParams {
108            owner: context.owner.clone(),
109            repo: context.repo.clone(),
110            pull_number,
111            body: Some(body),
112            event,
113            comments: Vec::new(),
114            commit_id: None,
115        };
116
117        let review = service.create_review(params).await?;
118
119        let event_label = match review.state {
120            ReviewState::Approved => "approved",
121            ReviewState::ChangesRequested => "requested changes on",
122            ReviewState::Commented | ReviewState::Dismissed | ReviewState::Pending => {
123                "commented on"
124            }
125        };
126
127        Ok(ActionResult::success(
128            format!("Created {} review on PR #{}", event_label, pull_number),
129            json!({
130                "id": review.id,
131                "state": review.state,
132                "html_url": review.html_url,
133                "body": review.body,
134            }),
135        ))
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use serde_json::json;
143
144    #[tokio::test]
145    async fn test_validate_with_review_keyword() {
146        let action = ReviewPullRequestAction;
147        let context = ActionContext {
148            message: json!({
149                "content": { "text": "Review pull request #42" }
150            }),
151            owner: "test".to_string(),
152            repo: "test".to_string(),
153            state: json!({}),
154        };
155
156        assert!(action.validate(&context).await.unwrap());
157    }
158
159    #[tokio::test]
160    async fn test_validate_with_approve_keyword() {
161        let action = ReviewPullRequestAction;
162        let context = ActionContext {
163            message: json!({
164                "content": { "text": "Approve the pull request" }
165            }),
166            owner: "test".to_string(),
167            repo: "test".to_string(),
168            state: json!({}),
169        };
170
171        assert!(action.validate(&context).await.unwrap());
172    }
173
174    #[tokio::test]
175    async fn test_validate_with_lgtm_keyword() {
176        let action = ReviewPullRequestAction;
177        let context = ActionContext {
178            message: json!({
179                "content": { "text": "LGTM on this PR" }
180            }),
181            owner: "test".to_string(),
182            repo: "test".to_string(),
183            state: json!({}),
184        };
185
186        assert!(action.validate(&context).await.unwrap());
187    }
188
189    #[tokio::test]
190    async fn test_validate_without_keywords() {
191        let action = ReviewPullRequestAction;
192        let context = ActionContext {
193            message: json!({
194                "content": { "text": "Hello world" }
195            }),
196            owner: "test".to_string(),
197            repo: "test".to_string(),
198            state: json!({}),
199        };
200
201        assert!(!action.validate(&context).await.unwrap());
202    }
203
204    #[test]
205    fn test_determine_review_event_approve() {
206        assert!(matches!(
207            ReviewPullRequestAction::determine_review_event("Approve this PR"),
208            ReviewEvent::Approve
209        ));
210        assert!(matches!(
211            ReviewPullRequestAction::determine_review_event("LGTM"),
212            ReviewEvent::Approve
213        ));
214        assert!(matches!(
215            ReviewPullRequestAction::determine_review_event("Looks good to me"),
216            ReviewEvent::Approve
217        ));
218    }
219
220    #[test]
221    fn test_determine_review_event_request_changes() {
222        assert!(matches!(
223            ReviewPullRequestAction::determine_review_event("Request changes on this"),
224            ReviewEvent::RequestChanges
225        ));
226        assert!(matches!(
227            ReviewPullRequestAction::determine_review_event("Needs work"),
228            ReviewEvent::RequestChanges
229        ));
230        assert!(matches!(
231            ReviewPullRequestAction::determine_review_event("Please fix this issue"),
232            ReviewEvent::RequestChanges
233        ));
234    }
235
236    #[test]
237    fn test_determine_review_event_comment() {
238        assert!(matches!(
239            ReviewPullRequestAction::determine_review_event("Just a comment"),
240            ReviewEvent::Comment
241        ));
242        assert!(matches!(
243            ReviewPullRequestAction::determine_review_event("What about this?"),
244            ReviewEvent::Comment
245        ));
246    }
247
248    #[test]
249    fn test_action_properties() {
250        let action = ReviewPullRequestAction;
251        assert_eq!(action.name(), "REVIEW_GITHUB_PULL_REQUEST");
252        assert!(!action.description().is_empty());
253        assert!(action.similes().contains(&"APPROVE_PR"));
254        assert!(action.similes().contains(&"CODE_REVIEW"));
255    }
256}