diff --git a/discussions.py b/discussions.py index b3ab1d0..7d2af43 100644 --- a/discussions.py +++ b/discussions.py @@ -31,9 +31,17 @@ def get_discussions(token: str, search_query: str, ghe: str): title url createdAt - comments(first: 1) { + author { + login + __typename + } + comments(first: 100) { nodes { createdAt + author { + login + __typename + } } } answerChosenAt diff --git a/most_active_mentors.py b/most_active_mentors.py index deb81eb..209dcaf 100755 --- a/most_active_mentors.py +++ b/most_active_mentors.py @@ -112,27 +112,36 @@ def count_comments_per_user( else: mentor_count[review_comment.user.login] = 1 - # The discussion branch below is dead in production (tracked in #774): - # the GraphQL query in discussions.get_discussions fetches no comment - # author data, attribute access on dict nodes would AttributeError, - # and the ignore_comment call below passes comment.user as both - # issue_user and comment_user (self-reference always True). - if discussion and len(discussion["comments"]["nodes"]) > 0: # pragma: no cover - for comment in discussion["comments"]["nodes"]: - if ignore_comment( - comment.user, - comment.user, - ignore_users, - comment.submitted_at, - comment.ready_for_review_at, - ): - continue + # The discussion branch: use dict access because GraphQL returns plain + # dicts (not github3 objects). Thread the discussion author as + # issue_user so we can filter out self-comments correctly. + if discussion and len(discussion["comments"]["nodes"]) > 0: + discussion_author_login = ( + discussion.get("author") or {} + ).get("login", "") + for comment in discussion["comments"]["nodes"]: + comment_author = comment.get("author") or {} + comment_login = comment_author.get("login", "") + comment_type = comment_author.get("__typename", "") + comment_created_at = comment.get("createdAt") + + if ( + not comment_login + # ignore bots + or comment_type == "Bot" + # ignore comments by the discussion author + or comment_login == discussion_author_login + # ignore users in the ignore list + or comment_login in ignore_users + # ignore comments without a timestamp + or not comment_created_at + ): + continue - # increase the number of comments left by current user by 1 - if comment.user.login in mentor_count: - mentor_count[comment.user.login] += 1 - else: - mentor_count[comment.user.login] = 1 + if comment_login in mentor_count: + mentor_count[comment_login] += 1 + else: + mentor_count[comment_login] = 1 return mentor_count diff --git a/test_most_active_mentors.py b/test_most_active_mentors.py index 1bcf4fd..8066f47 100755 --- a/test_most_active_mentors.py +++ b/test_most_active_mentors.py @@ -157,3 +157,126 @@ def test_count_comments_per_user_with_pr_reviews(self): result = count_comments_per_user(mock_issue, pull_request=mock_pr) self.assertEqual(result, {"reviewer": 2}) + + +class TestCountCommentsDiscussions(unittest.TestCase): + """Covers the discussion branch of count_comments_per_user. + + Before the fix for #774 this branch was dead code because: + 1. The GraphQL query fetched no comment author data. + 2. Attribute access on dict nodes would raise AttributeError. + 3. The ignore_comment call passed comment.user as both issue_user + and comment_user, so every comment was silently skipped. + """ + + def _make_discussion(self, author_login, comments): + """Return a minimal discussion dict as returned by get_discussions.""" + return { + "title": "Test discussion", + "url": "https://github.com/org/repo/discussions/1", + "createdAt": "2024-01-01T00:00:00Z", + "author": {"login": author_login, "__typename": "User"}, + "comments": {"nodes": comments}, + "answerChosenAt": None, + "closedAt": None, + } + + def test_counts_commenter(self): + """A comment by a user other than the author is counted.""" + discussion = self._make_discussion( + "op", + [ + { + "createdAt": "2024-01-02T00:00:00Z", + "author": {"login": "mentor", "__typename": "User"}, + }, + ], + ) + result = count_comments_per_user(None, discussion=discussion) + self.assertEqual(result, {"mentor": 1}) + + def test_ignores_discussion_author_self_comment(self): + """Comments by the discussion author are not counted.""" + discussion = self._make_discussion( + "op", + [ + { + "createdAt": "2024-01-02T00:00:00Z", + "author": {"login": "op", "__typename": "User"}, + }, + ], + ) + result = count_comments_per_user(None, discussion=discussion) + self.assertEqual(result, {}) + + def test_ignores_bot_commenter(self): + """Comments from Bot actors are not counted.""" + discussion = self._make_discussion( + "op", + [ + { + "createdAt": "2024-01-02T00:00:00Z", + "author": {"login": "github-actions", "__typename": "Bot"}, + }, + ], + ) + result = count_comments_per_user(None, discussion=discussion) + self.assertEqual(result, {}) + + def test_ignores_user_in_ignore_list(self): + """Comments by explicitly ignored users are not counted.""" + discussion = self._make_discussion( + "op", + [ + { + "createdAt": "2024-01-02T00:00:00Z", + "author": {"login": "spammer", "__typename": "User"}, + }, + ], + ) + result = count_comments_per_user( + None, discussion=discussion, ignore_users=["spammer"] + ) + self.assertEqual(result, {}) + + def test_multiple_commenters(self): + """Multiple distinct commenters each get their own count.""" + discussion = self._make_discussion( + "op", + [ + { + "createdAt": "2024-01-02T00:00:00Z", + "author": {"login": "alice", "__typename": "User"}, + }, + { + "createdAt": "2024-01-03T00:00:00Z", + "author": {"login": "bob", "__typename": "User"}, + }, + { + "createdAt": "2024-01-04T00:00:00Z", + "author": {"login": "alice", "__typename": "User"}, + }, + ], + ) + result = count_comments_per_user(None, discussion=discussion) + self.assertEqual(result, {"alice": 2, "bob": 1}) + + def test_no_comments(self): + """A discussion with an empty comments list returns an empty dict.""" + discussion = self._make_discussion("op", []) + result = count_comments_per_user(None, discussion=discussion) + self.assertEqual(result, {}) + + def test_null_author_skipped(self): + """A comment with a null author (deleted account) is skipped gracefully.""" + discussion = self._make_discussion( + "op", + [ + { + "createdAt": "2024-01-02T00:00:00Z", + "author": None, + }, + ], + ) + result = count_comments_per_user(None, discussion=discussion) + self.assertEqual(result, {})