diff --git a/app.py b/app.py index 638eac7..b07aa0a 100644 --- a/app.py +++ b/app.py @@ -41,20 +41,20 @@ github_bot.sync_existing_prs() -def reminder_scheduler(): - """Background thread to periodically check and send reminders.""" +def reviewer_assignment_scheduler(): + """Background thread to periodically check automatic reviewer assignment.""" with app.app_context(): while True: try: - github_bot.check_and_send_reminders() + github_bot.check_and_auto_assign_reviewers() except Exception as e: - logger.exception(f"Error in reminder scheduler: {str(e)}") + logger.exception(f"Error in reviewer assignment scheduler: {str(e)}") # Sleep for 1 minute before next check time.sleep(60) -# Start the reminder scheduler thread -reminder_thread = threading.Thread(target=reminder_scheduler, daemon=True) -reminder_thread.start() +# Start the reviewer assignment scheduler thread +reviewer_assignment_thread = threading.Thread(target=reviewer_assignment_scheduler, daemon=True) +reviewer_assignment_thread.start() @app.route('/') diff --git a/github_bot.py b/github_bot.py index 57b5ce4..decfd70 100644 --- a/github_bot.py +++ b/github_bot.py @@ -560,13 +560,12 @@ def auto_assign_reviewers(self, pr_record): except Exception as e: self.logger.exception(f"Error auto-assigning reviewers: {str(e)}") - def check_and_send_reminders(self): - """Check for PRs needing review reminders and auto-assign reviewers.""" - self.logger.info("Checking for PRs needing review reminders...") + def check_and_auto_assign_reviewers(self): + """Check for PRs needing automatic reviewer assignment.""" + self.logger.info("Checking for PRs needing automatic reviewer assignment...") current_time = datetime.utcnow() reviewer_threshold = current_time - timedelta(minutes=10) - reminder_threshold = current_time - timedelta(days=2) # Force a new connection from the pool self.db.session.remove() @@ -580,65 +579,6 @@ def check_and_send_reminders(self): for pr in prs_needing_assignment: self.auto_assign_reviewers(pr) - # Nag reviewers, but only on weekdays - now = datetime.utcnow() - if now.weekday() < 4 or (now.weekday() == 5 and now.hour < 17): - reviews_needing_reminders = Review.query.filter( - Review.completed_at.is_(None), - ((Review.last_reminder_sent.is_(None) & - (Review.requested_at <= reminder_threshold)) | - (Review.last_reminder_sent <= reminder_threshold)) - ).all() - - for review in reviews_needing_reminders: - self._send_review_reminder(review) - - def _send_review_reminder(self, review): - """Send a reminder comment on a PR.""" - try: - # Get assigned reviewers for the PR - repo_url = f"https://api.github.com/repos/{review.repo_name}" - pr_url = f"{repo_url}/pulls/{review.pr_number}" - - response = requests.get(pr_url, headers=self.headers) - response.raise_for_status() - - pr_data = response.json() - reviewers = [ - user['login'] - for user in pr_data.get('requested_reviewers', []) - ] - - if not reviewers: - self.logger.info(f"No reviewers to remind for PR #{review.pr_number}") - return - - # Create reminder message tagging all reviewers - reviewer_tags = ' '.join( - [f'@{reviewer}' for reviewer in reviewers]) - reminder_count = review.reminder_count + 1 - ordinal = lambda n: "%d%s" % (n, "tsnrhtdd"[ - (n // 10 % 10 != 1) * (n % 10 < 4) * n % 10::4]) - - message = ( - f"🔔 {ordinal(reminder_count)} Reminder\n\n" - f"Hey {reviewer_tags}! This PR has been waiting for your review.\n" - "Please take a look when you have a chance. If you're unable to review, " - "please let us know so we can find another reviewer.") - - # Post the reminder comment - self._create_comment(repo_url, review.pr_number, message) - - # Update reminder tracking - review.last_reminder_sent = datetime.utcnow() - review.reminder_count = reminder_count - self.db.session.commit() - - self.logger.info(f"Sent review reminder for PR #{review.pr_number}") - - except Exception as e: - self.logger.exception(f"Error sending reminder for PR #{review.pr_number}: {str(e)}") - def _has_bot_comment_about_second_reviewer(self, repo_url, pr_number): """Check if bot has already asked about second reviewer.""" # Get all comments on the PR diff --git a/models.py b/models.py index 2d613a2..1ef138b 100644 --- a/models.py +++ b/models.py @@ -37,8 +37,6 @@ class Review(db.Model): pr_number = db.Column(db.Integer, nullable=False) reviewer = db.Column(db.String(100), nullable=False) requested_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) - last_reminder_sent = db.Column(db.DateTime, nullable=True) - reminder_count = db.Column(db.Integer, default=0) completed_at = db.Column(db.DateTime, nullable=True) review_url = db.Column(db.String(200), nullable=True) __table_args__ = (db.ForeignKeyConstraint([repo_name, pr_number], diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8500fc6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Test package for unittest discovery. diff --git a/tests/test_review_reminders.py b/tests/test_review_reminders.py new file mode 100644 index 0000000..471be14 --- /dev/null +++ b/tests/test_review_reminders.py @@ -0,0 +1,85 @@ +import unittest +from datetime import datetime, timedelta +from unittest.mock import Mock, patch + +from flask import Flask + +from db import db +from github_bot import GitHubBot +from models import PRStatus, PullRequest, Review + + +class ReviewReminderTests(unittest.TestCase): + def setUp(self): + self.app = Flask(__name__) + self.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" + self.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + db.init_app(self.app) + self.app_context = self.app.app_context() + self.app_context.push() + db.create_all() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + def test_overdue_review_does_not_post_reminder_comment(self): + now = datetime(2026, 6, 23, 12, 0, 0) + pr = PullRequest( + repo_name="lightningdevkit/ldk-node", + pr_number=9999, + pr_title="Drop reminder comments", + status=PRStatus.PENDING_REVIEW, + created_at=now - timedelta(days=3), + ) + review = Review( + repo_name=pr.repo_name, + pr_number=pr.pr_number, + reviewer="tnull", + requested_at=now - timedelta(days=3), + ) + db.session.add_all([pr, review]) + db.session.commit() + + bot = GitHubBot("token", "secret", db) + pr_response = Mock() + pr_response.raise_for_status.return_value = None + pr_response.json.return_value = {"requested_reviewers": [{"login": review.reviewer}]} + + with patch("github_bot.datetime") as datetime_mock, \ + patch("github_bot.requests.get", return_value=pr_response), \ + patch.object(bot, "_create_comment") as create_comment: + datetime_mock.utcnow.return_value = now + + bot.check_and_auto_assign_reviewers() + + create_comment.assert_not_called() + + def test_unassigned_prs_are_still_auto_assigned_after_grace_period(self): + now = datetime(2026, 6, 23, 12, 0, 0) + pr_number = 10000 + pr = PullRequest( + repo_name="lightningdevkit/ldk-node", + pr_number=pr_number, + pr_title="Needs a reviewer", + status=PRStatus.PENDING_REVIEWER_CHOICE, + created_at=now - timedelta(minutes=11), + ) + db.session.add(pr) + db.session.commit() + + bot = GitHubBot("token", "secret", db) + + with patch("github_bot.datetime") as datetime_mock, \ + patch.object(bot, "auto_assign_reviewers") as auto_assign_reviewers: + datetime_mock.utcnow.return_value = now + + bot.check_and_auto_assign_reviewers() + + auto_assign_reviewers.assert_called_once() + self.assertEqual(auto_assign_reviewers.call_args.args[0].pr_number, pr_number) + + +if __name__ == "__main__": + unittest.main()