jleechanorg-pr-automation 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jleechanorg-pr-automation might be problematic. Click here for more details.

@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for Codex actor detection heuristics."""
3
+
4
+ import unittest
5
+
6
+ from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
7
+
8
+
9
+ class TestCodexActorMatching(unittest.TestCase):
10
+ """Validate detection of Codex-authored commits."""
11
+
12
+ def setUp(self) -> None:
13
+ self.monitor = JleechanorgPRMonitor()
14
+
15
+ def test_detects_codex_via_actor_fields(self) -> None:
16
+ commit_details = {
17
+ "author_login": "codex-bot",
18
+ "author_email": "codex@example.com",
19
+ "author_name": "Codex Bot",
20
+ "committer_login": None,
21
+ "committer_email": None,
22
+ "committer_name": None,
23
+ "message_headline": "Refactor subsystem",
24
+ "message": "Refactor subsystem",
25
+ }
26
+
27
+ self.assertTrue(
28
+ self.monitor._is_head_commit_from_codex(commit_details),
29
+ "Expected Codex detection when actor fields include Codex token",
30
+ )
31
+
32
+ def test_detects_codex_via_message_marker(self) -> None:
33
+ commit_details = {
34
+ "author_login": "regular-user",
35
+ "author_email": "dev@example.com",
36
+ "author_name": "Regular User",
37
+ "committer_login": "regular-user",
38
+ "committer_email": "dev@example.com",
39
+ "committer_name": "Regular User",
40
+ "message_headline": (
41
+ f"Address review feedback {self.monitor.CODEX_COMMIT_MESSAGE_MARKER}"
42
+ ),
43
+ "message": "Address review feedback and add tests",
44
+ }
45
+
46
+ self.assertTrue(
47
+ self.monitor._is_head_commit_from_codex(commit_details),
48
+ "Expected Codex detection from commit message marker",
49
+ )
50
+
51
+ def test_detects_codex_via_message_body_marker_case_insensitive(self) -> None:
52
+ commit_details = {
53
+ "author_login": "regular-user",
54
+ "author_email": "dev@example.com",
55
+ "author_name": "Regular User",
56
+ "committer_login": "regular-user",
57
+ "committer_email": "dev@example.com",
58
+ "committer_name": "Regular User",
59
+ "message_headline": "Address review feedback",
60
+ "message": "Add docs [CODEX-AUTOMATION-COMMIT] and clean up",
61
+ }
62
+
63
+ self.assertTrue(
64
+ self.monitor._is_head_commit_from_codex(commit_details),
65
+ "Expected Codex detection from commit body marker",
66
+ )
67
+
68
+ def test_returns_false_when_no_codex_tokens_found(self) -> None:
69
+ commit_details = {
70
+ "author_login": "regular-user",
71
+ "author_email": "dev@example.com",
72
+ "author_name": "Regular User",
73
+ "committer_login": "reviewer",
74
+ "committer_email": "reviewer@example.com",
75
+ "committer_name": "Helpful Reviewer",
76
+ "message_headline": "Refactor subsystem",
77
+ "message": "Improve code coverage",
78
+ }
79
+
80
+ self.assertFalse(
81
+ self.monitor._is_head_commit_from_codex(commit_details),
82
+ "Expected no Codex detection when no markers are present",
83
+ )
84
+
85
+ def test_handles_non_string_actor_fields(self) -> None:
86
+ """Type safety: should not crash when actor fields contain non-string values"""
87
+ commit_details = {
88
+ "author_login": {"nested": "value"}, # Invalid type
89
+ "author_email": 12345, # Invalid type
90
+ "author_name": None,
91
+ "committer_login": ["list", "value"], # Invalid type
92
+ "committer_email": None,
93
+ "committer_name": None,
94
+ "message_headline": "Normal message",
95
+ "message": "Normal body",
96
+ }
97
+
98
+ # Should not raise TypeError and should return False
99
+ result = self.monitor._is_head_commit_from_codex(commit_details)
100
+ self.assertFalse(result, "Should handle non-string fields gracefully")
101
+
102
+ def test_handles_non_string_message_fields(self) -> None:
103
+ """Type safety: should not crash when message fields contain non-string values"""
104
+ commit_details = {
105
+ "author_login": "regular-user",
106
+ "author_email": "dev@example.com",
107
+ "author_name": "Regular User",
108
+ "committer_login": None,
109
+ "committer_email": None,
110
+ "committer_name": None,
111
+ "message_headline": {"nested": "object"}, # Invalid type
112
+ "message": 12345, # Invalid type
113
+ }
114
+
115
+ # Should not raise TypeError and should return False
116
+ result = self.monitor._is_head_commit_from_codex(commit_details)
117
+ self.assertFalse(result, "Should handle non-string message fields gracefully")
118
+
119
+ def test_handles_empty_string_fields(self) -> None:
120
+ """Should handle empty strings correctly"""
121
+ commit_details = {
122
+ "author_login": "",
123
+ "author_email": "",
124
+ "author_name": "",
125
+ "committer_login": "",
126
+ "committer_email": "",
127
+ "committer_name": "",
128
+ "message_headline": "",
129
+ "message": "",
130
+ }
131
+
132
+ result = self.monitor._is_head_commit_from_codex(commit_details)
133
+ self.assertFalse(result, "Should treat empty strings as no Codex markers")
134
+
135
+
136
+ if __name__ == "__main__":
137
+ unittest.main()
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for GraphQL API error handling in head commit detection."""
3
+
4
+ import json
5
+ import subprocess
6
+ import unittest
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
10
+
11
+
12
+ class TestGraphQLErrorHandling(unittest.TestCase):
13
+ """Validate robust error handling for GraphQL API failures."""
14
+
15
+ def setUp(self) -> None:
16
+ self.monitor = JleechanorgPRMonitor()
17
+
18
+ @patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout')
19
+ def test_handles_api_timeout(self, mock_exec) -> None:
20
+ """Should return None when GraphQL API times out"""
21
+ mock_exec.side_effect = subprocess.TimeoutExpired(['gh'], 30)
22
+
23
+ result = self.monitor._get_head_commit_details('org/repo', 123)
24
+
25
+ self.assertIsNone(result, "Should return None on API timeout")
26
+
27
+ @patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout')
28
+ def test_handles_malformed_json(self, mock_exec) -> None:
29
+ """Should return None when GraphQL returns invalid JSON"""
30
+ mock_exec.return_value = MagicMock(
31
+ stdout='{"invalid": json, missing quotes}'
32
+ )
33
+
34
+ result = self.monitor._get_head_commit_details('org/repo', 123)
35
+
36
+ self.assertIsNone(result, "Should return None on malformed JSON")
37
+
38
+ @patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout')
39
+ def test_handles_missing_data_field(self, mock_exec) -> None:
40
+ """Should handle missing 'data' field in GraphQL response"""
41
+ mock_exec.return_value = MagicMock(
42
+ stdout='{"errors": [{"message": "Field error"}]}'
43
+ )
44
+
45
+ result = self.monitor._get_head_commit_details('org/repo', 123)
46
+
47
+ self.assertIsNone(result, "Should return None when data field missing")
48
+
49
+ @patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout')
50
+ def test_handles_missing_repository_field(self, mock_exec) -> None:
51
+ """Should handle missing 'repository' field gracefully"""
52
+ mock_exec.return_value = MagicMock(
53
+ stdout='{"data": {}}'
54
+ )
55
+
56
+ result = self.monitor._get_head_commit_details('org/repo', 123)
57
+
58
+ self.assertIsNone(result, "Should return None when repository field missing")
59
+
60
+ @patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout')
61
+ def test_handles_missing_commits(self, mock_exec) -> None:
62
+ """Should handle missing commits array gracefully"""
63
+ response = {
64
+ "data": {
65
+ "repository": {
66
+ "pullRequest": {}
67
+ }
68
+ }
69
+ }
70
+ mock_exec.return_value = MagicMock(stdout=json.dumps(response))
71
+
72
+ result = self.monitor._get_head_commit_details('org/repo', 123)
73
+
74
+ self.assertIsNone(result, "Should return None when commits missing")
75
+
76
+ @patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout')
77
+ def test_handles_empty_commits_array(self, mock_exec) -> None:
78
+ """Should handle empty commits array gracefully"""
79
+ response = {
80
+ "data": {
81
+ "repository": {
82
+ "pullRequest": {
83
+ "commits": {"nodes": []}
84
+ }
85
+ }
86
+ }
87
+ }
88
+ mock_exec.return_value = MagicMock(stdout=json.dumps(response))
89
+
90
+ result = self.monitor._get_head_commit_details('org/repo', 123)
91
+
92
+ self.assertIsNone(result, "Should return None when commits array empty")
93
+
94
+ @patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout')
95
+ def test_handles_called_process_error(self, mock_exec) -> None:
96
+ """Should handle subprocess CalledProcessError gracefully"""
97
+ mock_exec.side_effect = subprocess.CalledProcessError(
98
+ returncode=1,
99
+ cmd=['gh', 'api'],
100
+ stderr='API rate limit exceeded'
101
+ )
102
+
103
+ result = self.monitor._get_head_commit_details('org/repo', 123)
104
+
105
+ self.assertIsNone(result, "Should return None on CalledProcessError")
106
+
107
+ @patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout')
108
+ def test_handles_generic_exception(self, mock_exec) -> None:
109
+ """Should handle unexpected exceptions gracefully"""
110
+ mock_exec.side_effect = RuntimeError("Unexpected error")
111
+
112
+ result = self.monitor._get_head_commit_details('org/repo', 123)
113
+
114
+ self.assertIsNone(result, "Should return None on unexpected exception")
115
+
116
+ def test_validates_invalid_repo_format(self) -> None:
117
+ """Should return None for invalid repository format"""
118
+ result = self.monitor._get_head_commit_details('invalid-no-slash', 123)
119
+
120
+ self.assertIsNone(result, "Should reject repo without slash separator")
121
+
122
+ def test_validates_empty_repo_name(self) -> None:
123
+ """Should return None for empty repository parts"""
124
+ result = self.monitor._get_head_commit_details('/repo', 123)
125
+
126
+ self.assertIsNone(result, "Should reject empty owner")
127
+
128
+ def test_validates_invalid_github_owner_name(self) -> None:
129
+ """Should return None for invalid GitHub owner/repo names"""
130
+ # GitHub names cannot start with hyphen
131
+ result = self.monitor._get_head_commit_details('-invalid/repo', 123)
132
+
133
+ self.assertIsNone(result, "Should reject owner starting with hyphen")
134
+
135
+ def test_validates_invalid_pr_number_string(self) -> None:
136
+ """Should return None for non-integer PR number"""
137
+ result = self.monitor._get_head_commit_details('org/repo', "not-a-number")
138
+
139
+ self.assertIsNone(result, "Should reject string PR number")
140
+
141
+ def test_validates_negative_pr_number(self) -> None:
142
+ """Should return None for negative PR number"""
143
+ result = self.monitor._get_head_commit_details('org/repo', -1)
144
+
145
+ self.assertIsNone(result, "Should reject negative PR number")
146
+
147
+ def test_validates_zero_pr_number(self) -> None:
148
+ """Should return None for zero PR number"""
149
+ result = self.monitor._get_head_commit_details('org/repo', 0)
150
+
151
+ self.assertIsNone(result, "Should reject zero PR number")
152
+
153
+
154
+ if __name__ == "__main__":
155
+ unittest.main()