jleechanorg-pr-automation 0.1.1__py3-none-any.whl → 0.2.45__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.
Files changed (46) hide show
  1. jleechanorg_pr_automation/STORAGE_STATE_TESTING_PROTOCOL.md +326 -0
  2. jleechanorg_pr_automation/__init__.py +64 -9
  3. jleechanorg_pr_automation/automation_safety_manager.py +306 -95
  4. jleechanorg_pr_automation/automation_safety_wrapper.py +13 -19
  5. jleechanorg_pr_automation/automation_utils.py +87 -65
  6. jleechanorg_pr_automation/check_codex_comment.py +7 -1
  7. jleechanorg_pr_automation/codex_branch_updater.py +21 -9
  8. jleechanorg_pr_automation/codex_config.py +70 -3
  9. jleechanorg_pr_automation/jleechanorg_pr_monitor.py +1954 -234
  10. jleechanorg_pr_automation/logging_utils.py +86 -0
  11. jleechanorg_pr_automation/openai_automation/__init__.py +3 -0
  12. jleechanorg_pr_automation/openai_automation/codex_github_mentions.py +1111 -0
  13. jleechanorg_pr_automation/openai_automation/debug_page_content.py +88 -0
  14. jleechanorg_pr_automation/openai_automation/oracle_cli.py +364 -0
  15. jleechanorg_pr_automation/openai_automation/test_auth_restoration.py +244 -0
  16. jleechanorg_pr_automation/openai_automation/test_codex_comprehensive.py +355 -0
  17. jleechanorg_pr_automation/openai_automation/test_codex_integration.py +254 -0
  18. jleechanorg_pr_automation/orchestrated_pr_runner.py +516 -0
  19. jleechanorg_pr_automation/tests/__init__.py +0 -0
  20. jleechanorg_pr_automation/tests/test_actionable_counting_matrix.py +84 -86
  21. jleechanorg_pr_automation/tests/test_attempt_limit_logic.py +124 -0
  22. jleechanorg_pr_automation/tests/test_automation_marker_functions.py +175 -0
  23. jleechanorg_pr_automation/tests/test_automation_over_running_reproduction.py +9 -11
  24. jleechanorg_pr_automation/tests/test_automation_safety_limits.py +91 -79
  25. jleechanorg_pr_automation/tests/test_automation_safety_manager_comprehensive.py +53 -53
  26. jleechanorg_pr_automation/tests/test_codex_actor_matching.py +1 -1
  27. jleechanorg_pr_automation/tests/test_fixpr_prompt.py +54 -0
  28. jleechanorg_pr_automation/tests/test_fixpr_return_value.py +140 -0
  29. jleechanorg_pr_automation/tests/test_graphql_error_handling.py +26 -26
  30. jleechanorg_pr_automation/tests/test_model_parameter.py +317 -0
  31. jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +697 -0
  32. jleechanorg_pr_automation/tests/test_packaging_integration.py +127 -0
  33. jleechanorg_pr_automation/tests/test_pr_filtering_matrix.py +246 -193
  34. jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +354 -0
  35. jleechanorg_pr_automation/tests/test_pr_targeting.py +102 -7
  36. jleechanorg_pr_automation/tests/test_version_consistency.py +51 -0
  37. jleechanorg_pr_automation/tests/test_workflow_specific_limits.py +202 -0
  38. jleechanorg_pr_automation/tests/test_workspace_dispatch_missing_dir.py +119 -0
  39. jleechanorg_pr_automation/utils.py +81 -56
  40. jleechanorg_pr_automation-0.2.45.dist-info/METADATA +864 -0
  41. jleechanorg_pr_automation-0.2.45.dist-info/RECORD +45 -0
  42. jleechanorg_pr_automation-0.1.1.dist-info/METADATA +0 -222
  43. jleechanorg_pr_automation-0.1.1.dist-info/RECORD +0 -23
  44. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/WHEEL +0 -0
  45. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/entry_points.txt +0 -0
  46. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/top_level.txt +0 -0
@@ -8,12 +8,9 @@ Test Matrix Coverage:
8
8
  - Eligible PR Detection and Filtering
9
9
  """
10
10
 
11
- import os
12
- import unittest
13
11
  import tempfile
14
- import subprocess
15
- from datetime import datetime, timedelta
16
- from unittest.mock import Mock, patch, MagicMock
12
+ import unittest
13
+ from unittest.mock import Mock, patch
17
14
 
18
15
  from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
19
16
 
@@ -24,7 +21,7 @@ class TestPRFilteringMatrix(unittest.TestCase):
24
21
  def setUp(self):
25
22
  """Set up test environment"""
26
23
  self.temp_dir = tempfile.mkdtemp()
27
- self.monitor = JleechanorgPRMonitor()
24
+ self.monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
28
25
  self.monitor.history_storage_path = self.temp_dir
29
26
 
30
27
  def tearDown(self):
@@ -36,14 +33,14 @@ class TestPRFilteringMatrix(unittest.TestCase):
36
33
  def test_matrix_open_pr_new_commit_never_processed_should_be_actionable(self):
37
34
  """RED: Open PR with new commit, never processed → Should be actionable"""
38
35
  pr_data = {
39
- 'number': 1001,
40
- 'title': 'Test PR',
41
- 'state': 'open',
42
- 'isDraft': False,
43
- 'headRefName': 'feature-branch',
44
- 'repository': 'test-repo',
45
- 'repositoryFullName': 'org/test-repo',
46
- 'headRefOid': 'abc123new'
36
+ "number": 1001,
37
+ "title": "Test PR",
38
+ "state": "open",
39
+ "isDraft": False,
40
+ "headRefName": "feature-branch",
41
+ "repository": "test-repo",
42
+ "repositoryFullName": "org/test-repo",
43
+ "headRefOid": "abc123new"
47
44
  }
48
45
 
49
46
  # RED: This will fail - no is_pr_actionable method exists
@@ -53,18 +50,18 @@ class TestPRFilteringMatrix(unittest.TestCase):
53
50
  def test_matrix_open_pr_same_commit_already_processed_should_not_be_actionable(self):
54
51
  """RED: Open PR with same commit, already processed → Should not be actionable"""
55
52
  pr_data = {
56
- 'number': 1001,
57
- 'title': 'Test PR',
58
- 'state': 'open',
59
- 'isDraft': False,
60
- 'headRefName': 'feature-branch',
61
- 'repository': 'test-repo',
62
- 'repositoryFullName': 'org/test-repo',
63
- 'headRefOid': 'abc123same'
53
+ "number": 1001,
54
+ "title": "Test PR",
55
+ "state": "open",
56
+ "isDraft": False,
57
+ "headRefName": "feature-branch",
58
+ "repository": "test-repo",
59
+ "repositoryFullName": "org/test-repo",
60
+ "headRefOid": "abc123same"
64
61
  }
65
62
 
66
63
  # Simulate previous processing
67
- self.monitor._record_pr_processing('test-repo', 'feature-branch', 1001, 'abc123same')
64
+ self.monitor._record_pr_processing("test-repo", "feature-branch", 1001, "abc123same")
68
65
 
69
66
  # RED: This will fail - no is_pr_actionable method exists
70
67
  result = self.monitor.is_pr_actionable(pr_data)
@@ -73,18 +70,18 @@ class TestPRFilteringMatrix(unittest.TestCase):
73
70
  def test_matrix_open_pr_new_commit_old_commit_processed_should_be_actionable(self):
74
71
  """RED: Open PR with new commit, old commit processed → Should be actionable"""
75
72
  pr_data = {
76
- 'number': 1001,
77
- 'title': 'Test PR',
78
- 'state': 'open',
79
- 'isDraft': False,
80
- 'headRefName': 'feature-branch',
81
- 'repository': 'test-repo',
82
- 'repositoryFullName': 'org/test-repo',
83
- 'headRefOid': 'abc123new'
73
+ "number": 1001,
74
+ "title": "Test PR",
75
+ "state": "open",
76
+ "isDraft": False,
77
+ "headRefName": "feature-branch",
78
+ "repository": "test-repo",
79
+ "repositoryFullName": "org/test-repo",
80
+ "headRefOid": "abc123new"
84
81
  }
85
82
 
86
83
  # Simulate processing of old commit
87
- self.monitor._record_pr_processing('test-repo', 'feature-branch', 1001, 'abc123old')
84
+ self.monitor._record_pr_processing("test-repo", "feature-branch", 1001, "abc123old")
88
85
 
89
86
  # RED: This will fail - no is_pr_actionable method exists
90
87
  result = self.monitor.is_pr_actionable(pr_data)
@@ -93,48 +90,47 @@ class TestPRFilteringMatrix(unittest.TestCase):
93
90
  def test_matrix_closed_pr_any_commit_should_not_be_actionable(self):
94
91
  """RED: Closed PR with any commit → Should not be actionable"""
95
92
  pr_data = {
96
- 'number': 1001,
97
- 'title': 'Test PR',
98
- 'state': 'closed',
99
- 'isDraft': False,
100
- 'headRefName': 'feature-branch',
101
- 'repository': 'test-repo',
102
- 'repositoryFullName': 'org/test-repo',
103
- 'headRefOid': 'abc123new'
93
+ "number": 1001,
94
+ "title": "Test PR",
95
+ "state": "closed",
96
+ "isDraft": False,
97
+ "headRefName": "feature-branch",
98
+ "repository": "test-repo",
99
+ "repositoryFullName": "org/test-repo",
100
+ "headRefOid": "abc123new"
104
101
  }
105
102
 
106
103
  # RED: This will fail - no is_pr_actionable method exists
107
104
  result = self.monitor.is_pr_actionable(pr_data)
108
105
  self.assertFalse(result)
109
106
 
110
- def test_matrix_draft_pr_new_commit_never_processed_should_be_actionable(self):
111
- """RED: Draft PR with new commit, never processed → Should be actionable"""
107
+ def test_matrix_draft_pr_new_commit_never_processed_should_be_skipped(self):
108
+ """Draft PRs are skipped even with new commits"""
112
109
  pr_data = {
113
- 'number': 1001,
114
- 'title': 'Test PR',
115
- 'state': 'open',
116
- 'isDraft': True,
117
- 'headRefName': 'feature-branch',
118
- 'repository': 'test-repo',
119
- 'repositoryFullName': 'org/test-repo',
120
- 'headRefOid': 'abc123new'
110
+ "number": 1001,
111
+ "title": "Test PR",
112
+ "state": "open",
113
+ "isDraft": True,
114
+ "headRefName": "feature-branch",
115
+ "repository": "test-repo",
116
+ "repositoryFullName": "org/test-repo",
117
+ "headRefOid": "abc123new"
121
118
  }
122
119
 
123
- # RED: This will fail - no is_pr_actionable method exists
124
120
  result = self.monitor.is_pr_actionable(pr_data)
125
- self.assertTrue(result)
121
+ self.assertFalse(result)
126
122
 
127
123
  def test_matrix_open_pr_no_commits_should_not_be_actionable(self):
128
124
  """RED: Open PR with no commits → Should not be actionable"""
129
125
  pr_data = {
130
- 'number': 1001,
131
- 'title': 'Test PR',
132
- 'state': 'open',
133
- 'isDraft': False,
134
- 'headRefName': 'feature-branch',
135
- 'repository': 'test-repo',
136
- 'repositoryFullName': 'org/test-repo',
137
- 'headRefOid': None # No commits
126
+ "number": 1001,
127
+ "title": "Test PR",
128
+ "state": "open",
129
+ "isDraft": False,
130
+ "headRefName": "feature-branch",
131
+ "repository": "test-repo",
132
+ "repositoryFullName": "org/test-repo",
133
+ "headRefOid": None # No commits
138
134
  }
139
135
 
140
136
  # RED: This will fail - no is_pr_actionable method exists
@@ -148,14 +144,14 @@ class TestPRFilteringMatrix(unittest.TestCase):
148
144
  eligible_prs = []
149
145
  for i in range(15):
150
146
  pr = {
151
- 'number': 1000 + i,
152
- 'title': f'Test PR {i}',
153
- 'state': 'open',
154
- 'isDraft': False,
155
- 'headRefName': f'feature-branch-{i}',
156
- 'repository': 'test-repo',
157
- 'repositoryFullName': 'org/test-repo',
158
- 'headRefOid': f'abc123{i:03d}'
147
+ "number": 1000 + i,
148
+ "title": f"Test PR {i}",
149
+ "state": "open",
150
+ "isDraft": False,
151
+ "headRefName": f"feature-branch-{i}",
152
+ "repository": "test-repo",
153
+ "repositoryFullName": "org/test-repo",
154
+ "headRefOid": f"abc123{i:03d}"
159
155
  }
160
156
  eligible_prs.append(pr)
161
157
 
@@ -169,14 +165,14 @@ class TestPRFilteringMatrix(unittest.TestCase):
169
165
  eligible_prs = []
170
166
  for i in range(5):
171
167
  pr = {
172
- 'number': 1000 + i,
173
- 'title': f'Test PR {i}',
174
- 'state': 'open',
175
- 'isDraft': False,
176
- 'headRefName': f'feature-branch-{i}',
177
- 'repository': 'test-repo',
178
- 'repositoryFullName': 'org/test-repo',
179
- 'headRefOid': f'abc123{i:03d}'
168
+ "number": 1000 + i,
169
+ "title": f"Test PR {i}",
170
+ "state": "open",
171
+ "isDraft": False,
172
+ "headRefName": f"feature-branch-{i}",
173
+ "repository": "test-repo",
174
+ "repositoryFullName": "org/test-repo",
175
+ "headRefOid": f"abc123{i:03d}"
180
176
  }
181
177
  eligible_prs.append(pr)
182
178
 
@@ -200,31 +196,31 @@ class TestPRFilteringMatrix(unittest.TestCase):
200
196
  # 5 actionable PRs
201
197
  for i in range(5):
202
198
  pr = {
203
- 'number': 1000 + i,
204
- 'title': f'Actionable PR {i}',
205
- 'state': 'open',
206
- 'isDraft': False,
207
- 'headRefName': f'feature-branch-{i}',
208
- 'repository': 'test-repo',
209
- 'repositoryFullName': 'org/test-repo',
210
- 'headRefOid': f'abc123new{i:03d}'
199
+ "number": 1000 + i,
200
+ "title": f"Actionable PR {i}",
201
+ "state": "open",
202
+ "isDraft": False,
203
+ "headRefName": f"feature-branch-{i}",
204
+ "repository": "test-repo",
205
+ "repositoryFullName": "org/test-repo",
206
+ "headRefOid": f"abc123new{i:03d}"
211
207
  }
212
208
  all_prs.append(pr)
213
209
 
214
210
  # 3 already processed PRs (should be skipped)
215
211
  for i in range(3):
216
212
  pr = {
217
- 'number': 2000 + i,
218
- 'title': f'Processed PR {i}',
219
- 'state': 'open',
220
- 'isDraft': False,
221
- 'headRefName': f'processed-branch-{i}',
222
- 'repository': 'test-repo',
223
- 'repositoryFullName': 'org/test-repo',
224
- 'headRefOid': f'abc123old{i:03d}'
213
+ "number": 2000 + i,
214
+ "title": f"Processed PR {i}",
215
+ "state": "open",
216
+ "isDraft": False,
217
+ "headRefName": f"processed-branch-{i}",
218
+ "repository": "test-repo",
219
+ "repositoryFullName": "org/test-repo",
220
+ "headRefOid": f"abc123old{i:03d}"
225
221
  }
226
222
  # Pre-record as processed
227
- self.monitor._record_pr_processing('test-repo', f'processed-branch-{i}', 2000 + i, f'abc123old{i:03d}')
223
+ self.monitor._record_pr_processing("test-repo", f"processed-branch-{i}", 2000 + i, f"abc123old{i:03d}")
228
224
  all_prs.append(pr)
229
225
 
230
226
  # RED: This will fail - no filter_and_process_prs method exists
@@ -233,47 +229,46 @@ class TestPRFilteringMatrix(unittest.TestCase):
233
229
 
234
230
  # Matrix 3: Eligible PR Detection
235
231
  def test_matrix_filter_eligible_prs_from_mixed_list(self):
236
- """RED: Filter eligible PRs from mixed list Should return only actionable ones"""
232
+ """Filter eligible PRs from mixed list skips drafts"""
237
233
  mixed_prs = [
238
234
  # Actionable: Open, new commit
239
235
  {
240
- 'number': 1001, 'state': 'open', 'isDraft': False,
241
- 'headRefOid': 'new123', 'repository': 'repo1',
242
- 'headRefName': 'branch1', 'repositoryFullName': 'org/repo1'
236
+ "number": 1001, "state": "open", "isDraft": False,
237
+ "headRefOid": "new123", "repository": "repo1",
238
+ "headRefName": "branch1", "repositoryFullName": "org/repo1"
243
239
  },
244
240
  # Not actionable: Closed
245
241
  {
246
- 'number': 1002, 'state': 'closed', 'isDraft': False,
247
- 'headRefOid': 'new456', 'repository': 'repo2',
248
- 'headRefName': 'branch2', 'repositoryFullName': 'org/repo2'
242
+ "number": 1002, "state": "closed", "isDraft": False,
243
+ "headRefOid": "new456", "repository": "repo2",
244
+ "headRefName": "branch2", "repositoryFullName": "org/repo2"
249
245
  },
250
246
  # Not actionable: Already processed
251
247
  {
252
- 'number': 1003, 'state': 'open', 'isDraft': False,
253
- 'headRefOid': 'old789', 'repository': 'repo3',
254
- 'headRefName': 'branch3', 'repositoryFullName': 'org/repo3'
248
+ "number": 1003, "state": "open", "isDraft": False,
249
+ "headRefOid": "old789", "repository": "repo3",
250
+ "headRefName": "branch3", "repositoryFullName": "org/repo3"
255
251
  },
256
- # Actionable: Draft but new commit
252
+ # Skipped: Draft even with new commit
257
253
  {
258
- 'number': 1004, 'state': 'open', 'isDraft': True,
259
- 'headRefOid': 'new999', 'repository': 'repo4',
260
- 'headRefName': 'branch4', 'repositoryFullName': 'org/repo4'
254
+ "number": 1004, "state": "open", "isDraft": True,
255
+ "headRefOid": "new999", "repository": "repo4",
256
+ "headRefName": "branch4", "repositoryFullName": "org/repo4"
261
257
  }
262
258
  ]
263
259
 
264
260
  # Mark one as already processed
265
- self.monitor._record_pr_processing('repo3', 'branch3', 1003, 'old789')
261
+ self.monitor._record_pr_processing("repo3", "branch3", 1003, "old789")
266
262
 
267
- # RED: This will fail - no filter_eligible_prs method exists
268
263
  eligible_prs = self.monitor.filter_eligible_prs(mixed_prs)
269
264
 
270
- # Should return only the 2 actionable PRs
271
- self.assertEqual(len(eligible_prs), 2)
272
- actionable_numbers = [pr['number'] for pr in eligible_prs]
265
+ # Should return only the 1 actionable PR (draft skipped)
266
+ self.assertEqual(len(eligible_prs), 1)
267
+ actionable_numbers = [pr["number"] for pr in eligible_prs]
273
268
  self.assertIn(1001, actionable_numbers)
274
- self.assertIn(1004, actionable_numbers)
275
269
  self.assertNotIn(1002, actionable_numbers) # Closed
276
270
  self.assertNotIn(1003, actionable_numbers) # Already processed
271
+ self.assertNotIn(1004, actionable_numbers) # Draft skipped
277
272
 
278
273
  def test_matrix_find_5_eligible_prs_from_live_data(self):
279
274
  """RED: Find 5 eligible PRs from live GitHub data → Should return 5 actionable PRs"""
@@ -288,7 +283,7 @@ class TestPRFilteringMatrix(unittest.TestCase):
288
283
  {"number": 7, "state": "open", "isDraft": False, "headRefOid": "stu901", "repository": "repo7", "headRefName": "feature7"}
289
284
  ]
290
285
 
291
- with patch.object(self.monitor, 'discover_open_prs', return_value=mock_prs):
286
+ with patch.object(self.monitor, "discover_open_prs", return_value=mock_prs):
292
287
  eligible_prs = self.monitor.find_eligible_prs(limit=5)
293
288
  self.assertEqual(len(eligible_prs), 5)
294
289
  # All returned PRs should be actionable
@@ -298,160 +293,160 @@ class TestPRFilteringMatrix(unittest.TestCase):
298
293
  # Matrix 5: Comment Posting Return Values (Bug Fix Tests)
299
294
  def test_comment_posting_returns_posted_on_success(self):
300
295
  """GREEN: Comment posting should return 'posted' when successful"""
301
- with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
302
- patch.object(self.monitor, '_should_skip_pr') as mock_skip, \
303
- patch.object(self.monitor, '_has_codex_comment_for_commit') as mock_has_comment, \
304
- patch.object(self.monitor, '_build_codex_comment_body_simple') as mock_build_body, \
305
- patch.object(self.monitor, '_record_processed_pr') as mock_record, \
306
- patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout') as mock_subprocess:
296
+ with patch.object(self.monitor, "_get_pr_comment_state") as mock_state, \
297
+ patch.object(self.monitor, "_should_skip_pr") as mock_skip, \
298
+ patch.object(self.monitor, "_has_codex_comment_for_commit") as mock_has_comment, \
299
+ patch.object(self.monitor, "_build_codex_comment_body_simple") as mock_build_body, \
300
+ patch.object(self.monitor, "_record_processed_pr") as mock_record, \
301
+ patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout") as mock_subprocess:
307
302
 
308
303
  # Setup: PR not skipped, no existing comment, successful command
309
- mock_state.return_value = ('sha123', [])
304
+ mock_state.return_value = ("sha123", [])
310
305
  mock_skip.return_value = False
311
306
  mock_has_comment.return_value = False
312
- mock_build_body.return_value = 'Test comment body'
313
- mock_subprocess.return_value = Mock(returncode=0, stdout='success', stderr='')
307
+ mock_build_body.return_value = "Test comment body"
308
+ mock_subprocess.return_value = Mock(returncode=0, stdout="success", stderr="")
314
309
 
315
310
  pr_data = {
316
- 'repositoryFullName': 'org/repo',
317
- 'headRefName': 'feature'
311
+ "repositoryFullName": "org/repo",
312
+ "headRefName": "feature"
318
313
  }
319
314
 
320
- result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
321
- self.assertEqual(result, 'posted')
315
+ result = self.monitor.post_codex_instruction_simple("org/repo", 123, pr_data)
316
+ self.assertEqual(result, "posted")
322
317
  mock_record.assert_called_once()
323
318
 
324
319
  def test_comment_posting_returns_skipped_when_already_processed(self):
325
320
  """GREEN: Comment posting should return 'skipped' when PR already processed"""
326
- with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
327
- patch.object(self.monitor, '_should_skip_pr') as mock_skip:
321
+ with patch.object(self.monitor, "_get_pr_comment_state") as mock_state, \
322
+ patch.object(self.monitor, "_should_skip_pr") as mock_skip:
328
323
 
329
324
  # Setup: PR should be skipped
330
- mock_state.return_value = ('sha123', [])
325
+ mock_state.return_value = ("sha123", [])
331
326
  mock_skip.return_value = True
332
327
 
333
328
  pr_data = {
334
- 'repositoryFullName': 'org/repo',
335
- 'headRefName': 'feature'
329
+ "repositoryFullName": "org/repo",
330
+ "headRefName": "feature"
336
331
  }
337
332
 
338
- result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
339
- self.assertEqual(result, 'skipped')
333
+ result = self.monitor.post_codex_instruction_simple("org/repo", 123, pr_data)
334
+ self.assertEqual(result, "skipped")
340
335
 
341
336
  def test_comment_posting_returns_skipped_when_comment_exists(self):
342
337
  """GREEN: Comment posting should return 'skipped' when comment already exists for commit"""
343
- with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
344
- patch.object(self.monitor, '_should_skip_pr') as mock_skip, \
345
- patch.object(self.monitor, '_has_codex_comment_for_commit') as mock_has_comment:
338
+ with patch.object(self.monitor, "_get_pr_comment_state") as mock_state, \
339
+ patch.object(self.monitor, "_should_skip_pr") as mock_skip, \
340
+ patch.object(self.monitor, "_has_codex_comment_for_commit") as mock_has_comment:
346
341
 
347
342
  # Setup: PR not skipped but has existing comment
348
- mock_state.return_value = ('sha123', [])
343
+ mock_state.return_value = ("sha123", [])
349
344
  mock_skip.return_value = False
350
345
  mock_has_comment.return_value = True
351
346
 
352
347
  pr_data = {
353
- 'repositoryFullName': 'org/repo',
354
- 'headRefName': 'feature'
348
+ "repositoryFullName": "org/repo",
349
+ "headRefName": "feature"
355
350
  }
356
351
 
357
- result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
358
- self.assertEqual(result, 'skipped')
352
+ result = self.monitor.post_codex_instruction_simple("org/repo", 123, pr_data)
353
+ self.assertEqual(result, "skipped")
359
354
 
360
355
  def test_comment_posting_returns_failed_on_subprocess_error(self):
361
356
  """GREEN: Comment posting should return 'failed' when subprocess fails"""
362
- with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
363
- patch.object(self.monitor, '_should_skip_pr') as mock_skip, \
364
- patch.object(self.monitor, '_has_codex_comment_for_commit') as mock_has_comment, \
365
- patch.object(self.monitor, '_build_codex_comment_body_simple') as mock_build_body, \
366
- patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout') as mock_subprocess:
357
+ with patch.object(self.monitor, "_get_pr_comment_state") as mock_state, \
358
+ patch.object(self.monitor, "_should_skip_pr") as mock_skip, \
359
+ patch.object(self.monitor, "_has_codex_comment_for_commit") as mock_has_comment, \
360
+ patch.object(self.monitor, "_build_codex_comment_body_simple") as mock_build_body, \
361
+ patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout") as mock_subprocess:
367
362
 
368
363
  # Setup: PR not skipped, no existing comment, but command fails
369
- mock_state.return_value = ('sha123', [])
364
+ mock_state.return_value = ("sha123", [])
370
365
  mock_skip.return_value = False
371
366
  mock_has_comment.return_value = False
372
- mock_build_body.return_value = 'Test comment body'
373
- mock_subprocess.side_effect = Exception('Command failed')
367
+ mock_build_body.return_value = "Test comment body"
368
+ mock_subprocess.side_effect = Exception("Command failed")
374
369
 
375
370
  pr_data = {
376
- 'repositoryFullName': 'org/repo',
377
- 'headRefName': 'feature'
371
+ "repositoryFullName": "org/repo",
372
+ "headRefName": "feature"
378
373
  }
379
374
 
380
- result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
381
- self.assertEqual(result, 'failed')
375
+ result = self.monitor.post_codex_instruction_simple("org/repo", 123, pr_data)
376
+ self.assertEqual(result, "failed")
382
377
 
383
378
  def test_comment_posting_skips_when_head_commit_from_codex(self):
384
379
  """GREEN: post_codex_instruction_simple should skip when head commit is Codex-attributed"""
385
- with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
386
- patch.object(self.monitor, '_get_head_commit_details') as mock_head_details, \
387
- patch.object(self.monitor, '_is_head_commit_from_codex') as mock_is_codex, \
388
- patch.object(self.monitor, '_should_skip_pr') as mock_should_skip, \
389
- patch.object(self.monitor, '_has_codex_comment_for_commit') as mock_has_comment, \
390
- patch.object(self.monitor, '_record_processed_pr') as mock_record_processed, \
391
- patch.object(self.monitor, '_build_codex_comment_body_simple') as mock_build_body, \
392
- patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout') as mock_subprocess:
393
-
394
- mock_state.return_value = ('sha123', [])
395
- mock_head_details.return_value = {'sha': 'sha123'}
380
+ with patch.object(self.monitor, "_get_pr_comment_state") as mock_state, \
381
+ patch.object(self.monitor, "_get_head_commit_details") as mock_head_details, \
382
+ patch.object(self.monitor, "_is_head_commit_from_codex") as mock_is_codex, \
383
+ patch.object(self.monitor, "_should_skip_pr") as mock_should_skip, \
384
+ patch.object(self.monitor, "_has_codex_comment_for_commit") as mock_has_comment, \
385
+ patch.object(self.monitor, "_record_processed_pr") as mock_record_processed, \
386
+ patch.object(self.monitor, "_build_codex_comment_body_simple") as mock_build_body, \
387
+ patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout") as mock_subprocess:
388
+
389
+ mock_state.return_value = ("sha123", [])
390
+ mock_head_details.return_value = {"sha": "sha123"}
396
391
  mock_is_codex.return_value = True
397
392
 
398
393
  pr_data = {
399
- 'repositoryFullName': 'org/repo',
400
- 'headRefName': 'feature',
394
+ "repositoryFullName": "org/repo",
395
+ "headRefName": "feature",
401
396
  }
402
397
 
403
- result = self.monitor.post_codex_instruction_simple('org/repo', 456, pr_data)
398
+ result = self.monitor.post_codex_instruction_simple("org/repo", 456, pr_data)
404
399
 
405
- self.assertEqual(result, 'skipped')
406
- mock_is_codex.assert_called_once_with({'sha': 'sha123'})
400
+ self.assertEqual(result, "skipped")
401
+ mock_is_codex.assert_called_once_with({"sha": "sha123"})
407
402
  mock_should_skip.assert_not_called()
408
403
  mock_has_comment.assert_not_called()
409
404
  mock_build_body.assert_not_called()
410
405
  mock_subprocess.assert_not_called()
411
- mock_record_processed.assert_called_once_with('repo', 'feature', 456, 'sha123')
406
+ mock_record_processed.assert_called_once_with("repo", "feature", 456, "sha123")
412
407
 
413
408
  def test_process_pr_comment_only_returns_true_for_posted(self):
414
409
  """GREEN: _process_pr_comment should only return True when comment actually posted"""
415
- with patch.object(self.monitor, 'post_codex_instruction_simple') as mock_post:
410
+ with patch.object(self.monitor, "post_codex_instruction_simple") as mock_post:
416
411
 
417
- pr_data = {'repositoryFullName': 'org/repo'}
412
+ pr_data = {"repositoryFullName": "org/repo"}
418
413
 
419
414
  # Test: Returns True only for 'posted'
420
- mock_post.return_value = 'posted'
421
- self.assertTrue(self.monitor._process_pr_comment('repo', 123, pr_data))
415
+ mock_post.return_value = "posted"
416
+ self.assertTrue(self.monitor._process_pr_comment("repo", 123, pr_data))
422
417
 
423
418
  # Test: Returns False for 'skipped'
424
- mock_post.return_value = 'skipped'
425
- self.assertFalse(self.monitor._process_pr_comment('repo', 123, pr_data))
419
+ mock_post.return_value = "skipped"
420
+ self.assertFalse(self.monitor._process_pr_comment("repo", 123, pr_data))
426
421
 
427
422
  # Test: Returns False for 'failed'
428
- mock_post.return_value = 'failed'
429
- self.assertFalse(self.monitor._process_pr_comment('repo', 123, pr_data))
423
+ mock_post.return_value = "failed"
424
+ self.assertFalse(self.monitor._process_pr_comment("repo", 123, pr_data))
430
425
 
431
426
  def test_comment_template_contains_all_ai_assistants(self):
432
427
  """GREEN: Comment template should mention all 4 AI assistants"""
433
428
  pr_data = {
434
- 'title': 'Test PR',
435
- 'author': {'login': 'testuser'},
436
- 'headRefName': 'test-branch'
429
+ "title": "Test PR",
430
+ "author": {"login": "testuser"},
431
+ "headRefName": "test-branch"
437
432
  }
438
433
 
439
434
  comment_body = self.monitor._build_codex_comment_body_simple(
440
- 'test/repo', 123, pr_data, 'abc12345'
435
+ "test/repo", 123, pr_data, "abc12345"
441
436
  )
442
437
 
443
438
  # Verify all 4 AI assistant mentions are present
444
- self.assertIn('@codex', comment_body, "Comment should mention @codex")
445
- self.assertIn('@coderabbitai', comment_body, "Comment should mention @coderabbitai")
446
- self.assertIn('@copilot', comment_body, "Comment should mention @copilot")
447
- self.assertIn('@cursor', comment_body, "Comment should mention @cursor")
439
+ self.assertIn("@codex", comment_body, "Comment should mention @codex")
440
+ self.assertIn("@coderabbitai", comment_body, "Comment should mention @coderabbitai")
441
+ self.assertIn("@copilot", comment_body, "Comment should mention @copilot")
442
+ self.assertIn("@cursor", comment_body, "Comment should mention @cursor")
448
443
 
449
444
  # Verify they appear at the beginning of the comment
450
- first_line = comment_body.split('\n')[0]
451
- self.assertIn('@codex', first_line, "@codex should be in first line")
452
- self.assertIn('@coderabbitai', first_line, "@coderabbitai should be in first line")
453
- self.assertIn('@copilot', first_line, "@copilot should be in first line")
454
- self.assertIn('@cursor', first_line, "@cursor should be in first line")
445
+ first_line = comment_body.split("\n")[0]
446
+ self.assertIn("@codex", first_line, "@codex should be in first line")
447
+ self.assertIn("@coderabbitai", first_line, "@coderabbitai should be in first line")
448
+ self.assertIn("@copilot", first_line, "@copilot should be in first line")
449
+ self.assertIn("@cursor", first_line, "@cursor should be in first line")
455
450
 
456
451
  # Verify automation marker instructions are documented
457
452
  self.assertIn(
@@ -465,8 +460,66 @@ class TestPRFilteringMatrix(unittest.TestCase):
465
460
  "Comment should remind Codex about the hidden commit marker",
466
461
  )
467
462
 
463
+ def test_fix_comment_review_body_includes_greptile(self):
464
+ """Fix-comment review body should include Greptile + standard bot mentions."""
465
+ pr_data = {
466
+ "title": "Test PR",
467
+ "author": {"login": "dev"},
468
+ "headRefName": "feature-branch",
469
+ }
470
+
471
+ comment_body = self.monitor._build_fix_comment_review_body(
472
+ "org/repo",
473
+ 123,
474
+ pr_data,
475
+ "abc123",
476
+ )
477
+
478
+ self.assertIn("@greptile", comment_body)
479
+ self.assertIn("@codex", comment_body)
480
+ self.assertIn(self.monitor.FIX_COMMENT_MARKER_PREFIX, comment_body)
481
+
482
+ def test_fix_comment_prompt_requires_gh_comment_replies(self):
483
+ """Fix-comment prompt should require gh pr comment replies for 100% of comments."""
484
+ pr_data = {
485
+ "title": "Test PR",
486
+ "author": {"login": "dev"},
487
+ "headRefName": "feature-branch",
488
+ }
489
+
490
+ prompt_body = self.monitor._build_fix_comment_prompt_body(
491
+ "org/repo",
492
+ 123,
493
+ pr_data,
494
+ "abc123",
495
+ "gemini",
496
+ )
497
+
498
+ self.assertIn("gh pr comment", prompt_body)
499
+ self.assertIn("reply to **100%** of comments INDIVIDUALLY", prompt_body)
500
+
501
+ def test_fix_comment_mode_dispatches_agent(self):
502
+ """Fix-comment processing should dispatch orchestration agent and post comments."""
503
+ pr_data = {
504
+ "title": "Test PR",
505
+ "author": {"login": "dev"},
506
+ "headRefName": "feature-branch",
507
+ "repositoryFullName": "org/repo",
508
+ }
509
+
510
+ with patch.object(self.monitor, "_get_pr_comment_state", return_value=("abc123", [])), \
511
+ patch.object(self.monitor, "_should_skip_pr", return_value=False), \
512
+ patch.object(self.monitor, "_has_fix_comment_comment_for_commit", return_value=False), \
513
+ patch.object(self.monitor, "dispatch_fix_comment_agent", return_value=True), \
514
+ patch.object(self.monitor, "_post_fix_comment_queued", return_value=True), \
515
+ patch.object(self.monitor, "_start_fix_comment_review_watcher", return_value=True) as mock_start:
516
+
517
+ result = self.monitor._process_pr_fix_comment("org/repo", 123, pr_data, agent_cli="gemini")
518
+ self.assertEqual(result, "posted")
519
+ mock_start.assert_called_once()
520
+
468
521
 
469
- if __name__ == '__main__':
522
+ if __name__ == "__main__":
470
523
  # RED Phase: Run tests to confirm they FAIL
471
524
  print("🔴 RED Phase: Running failing tests for PR filtering matrix")
472
525
  print("Expected: ALL TESTS SHOULD FAIL (no implementation exists)")