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.
- jleechanorg_pr_automation/__init__.py +32 -0
- jleechanorg_pr_automation/automation_safety_manager.py +700 -0
- jleechanorg_pr_automation/automation_safety_wrapper.py +116 -0
- jleechanorg_pr_automation/automation_utils.py +314 -0
- jleechanorg_pr_automation/check_codex_comment.py +76 -0
- jleechanorg_pr_automation/codex_branch_updater.py +272 -0
- jleechanorg_pr_automation/codex_config.py +57 -0
- jleechanorg_pr_automation/jleechanorg_pr_monitor.py +1202 -0
- jleechanorg_pr_automation/tests/conftest.py +12 -0
- jleechanorg_pr_automation/tests/test_actionable_counting_matrix.py +221 -0
- jleechanorg_pr_automation/tests/test_automation_over_running_reproduction.py +147 -0
- jleechanorg_pr_automation/tests/test_automation_safety_limits.py +340 -0
- jleechanorg_pr_automation/tests/test_automation_safety_manager_comprehensive.py +615 -0
- jleechanorg_pr_automation/tests/test_codex_actor_matching.py +137 -0
- jleechanorg_pr_automation/tests/test_graphql_error_handling.py +155 -0
- jleechanorg_pr_automation/tests/test_pr_filtering_matrix.py +473 -0
- jleechanorg_pr_automation/tests/test_pr_targeting.py +95 -0
- jleechanorg_pr_automation/utils.py +232 -0
- jleechanorg_pr_automation-0.1.0.dist-info/METADATA +217 -0
- jleechanorg_pr_automation-0.1.0.dist-info/RECORD +23 -0
- jleechanorg_pr_automation-0.1.0.dist-info/WHEEL +5 -0
- jleechanorg_pr_automation-0.1.0.dist-info/entry_points.txt +3 -0
- jleechanorg_pr_automation-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
RED Phase: Matrix-driven TDD tests for PR filtering and actionable counting
|
|
4
|
+
|
|
5
|
+
Test Matrix Coverage:
|
|
6
|
+
- PR Status × Commit Changes × Processing History → Action + Count
|
|
7
|
+
- Batch Processing Logic with Skip Exclusion
|
|
8
|
+
- Eligible PR Detection and Filtering
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import unittest
|
|
13
|
+
import tempfile
|
|
14
|
+
import subprocess
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
17
|
+
|
|
18
|
+
from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestPRFilteringMatrix(unittest.TestCase):
|
|
22
|
+
"""Matrix testing for PR filtering and actionable counting logic"""
|
|
23
|
+
|
|
24
|
+
def setUp(self):
|
|
25
|
+
"""Set up test environment"""
|
|
26
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
27
|
+
self.monitor = JleechanorgPRMonitor()
|
|
28
|
+
self.monitor.history_storage_path = self.temp_dir
|
|
29
|
+
|
|
30
|
+
def tearDown(self):
|
|
31
|
+
"""Clean up test files"""
|
|
32
|
+
import shutil
|
|
33
|
+
shutil.rmtree(self.temp_dir)
|
|
34
|
+
|
|
35
|
+
# Matrix 1: PR Status × Commit Changes × Processing History
|
|
36
|
+
def test_matrix_open_pr_new_commit_never_processed_should_be_actionable(self):
|
|
37
|
+
"""RED: Open PR with new commit, never processed → Should be actionable"""
|
|
38
|
+
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'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# RED: This will fail - no is_pr_actionable method exists
|
|
50
|
+
result = self.monitor.is_pr_actionable(pr_data)
|
|
51
|
+
self.assertTrue(result)
|
|
52
|
+
|
|
53
|
+
def test_matrix_open_pr_same_commit_already_processed_should_not_be_actionable(self):
|
|
54
|
+
"""RED: Open PR with same commit, already processed → Should not be actionable"""
|
|
55
|
+
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'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Simulate previous processing
|
|
67
|
+
self.monitor._record_pr_processing('test-repo', 'feature-branch', 1001, 'abc123same')
|
|
68
|
+
|
|
69
|
+
# RED: This will fail - no is_pr_actionable method exists
|
|
70
|
+
result = self.monitor.is_pr_actionable(pr_data)
|
|
71
|
+
self.assertFalse(result)
|
|
72
|
+
|
|
73
|
+
def test_matrix_open_pr_new_commit_old_commit_processed_should_be_actionable(self):
|
|
74
|
+
"""RED: Open PR with new commit, old commit processed → Should be actionable"""
|
|
75
|
+
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'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Simulate processing of old commit
|
|
87
|
+
self.monitor._record_pr_processing('test-repo', 'feature-branch', 1001, 'abc123old')
|
|
88
|
+
|
|
89
|
+
# RED: This will fail - no is_pr_actionable method exists
|
|
90
|
+
result = self.monitor.is_pr_actionable(pr_data)
|
|
91
|
+
self.assertTrue(result)
|
|
92
|
+
|
|
93
|
+
def test_matrix_closed_pr_any_commit_should_not_be_actionable(self):
|
|
94
|
+
"""RED: Closed PR with any commit → Should not be actionable"""
|
|
95
|
+
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'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# RED: This will fail - no is_pr_actionable method exists
|
|
107
|
+
result = self.monitor.is_pr_actionable(pr_data)
|
|
108
|
+
self.assertFalse(result)
|
|
109
|
+
|
|
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"""
|
|
112
|
+
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'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# RED: This will fail - no is_pr_actionable method exists
|
|
124
|
+
result = self.monitor.is_pr_actionable(pr_data)
|
|
125
|
+
self.assertTrue(result)
|
|
126
|
+
|
|
127
|
+
def test_matrix_open_pr_no_commits_should_not_be_actionable(self):
|
|
128
|
+
"""RED: Open PR with no commits → Should not be actionable"""
|
|
129
|
+
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
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# RED: This will fail - no is_pr_actionable method exists
|
|
141
|
+
result = self.monitor.is_pr_actionable(pr_data)
|
|
142
|
+
self.assertFalse(result)
|
|
143
|
+
|
|
144
|
+
# Matrix 2: Batch Processing Logic with Skip Exclusion
|
|
145
|
+
def test_matrix_batch_processing_15_eligible_target_10_should_process_10(self):
|
|
146
|
+
"""RED: 15 eligible PRs, target 10 → Should process exactly 10"""
|
|
147
|
+
# Create 15 eligible PRs
|
|
148
|
+
eligible_prs = []
|
|
149
|
+
for i in range(15):
|
|
150
|
+
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}'
|
|
159
|
+
}
|
|
160
|
+
eligible_prs.append(pr)
|
|
161
|
+
|
|
162
|
+
# RED: This will fail - no process_actionable_prs method exists
|
|
163
|
+
processed_count = self.monitor.process_actionable_prs(eligible_prs, target_count=10)
|
|
164
|
+
self.assertEqual(processed_count, 10)
|
|
165
|
+
|
|
166
|
+
def test_matrix_batch_processing_5_eligible_target_10_should_process_5(self):
|
|
167
|
+
"""RED: 5 eligible PRs, target 10 → Should process all 5"""
|
|
168
|
+
# Create 5 eligible PRs
|
|
169
|
+
eligible_prs = []
|
|
170
|
+
for i in range(5):
|
|
171
|
+
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}'
|
|
180
|
+
}
|
|
181
|
+
eligible_prs.append(pr)
|
|
182
|
+
|
|
183
|
+
# RED: This will fail - no process_actionable_prs method exists
|
|
184
|
+
processed_count = self.monitor.process_actionable_prs(eligible_prs, target_count=10)
|
|
185
|
+
self.assertEqual(processed_count, 5)
|
|
186
|
+
|
|
187
|
+
def test_matrix_batch_processing_0_eligible_target_10_should_process_0(self):
|
|
188
|
+
"""RED: 0 eligible PRs, target 10 → Should process 0"""
|
|
189
|
+
eligible_prs = []
|
|
190
|
+
|
|
191
|
+
# RED: This will fail - no process_actionable_prs method exists
|
|
192
|
+
processed_count = self.monitor.process_actionable_prs(eligible_prs, target_count=10)
|
|
193
|
+
self.assertEqual(processed_count, 0)
|
|
194
|
+
|
|
195
|
+
def test_matrix_batch_processing_mixed_actionable_and_skipped_should_exclude_skipped_from_count(self):
|
|
196
|
+
"""RED: Mixed actionable and skipped PRs → Should exclude skipped from count"""
|
|
197
|
+
# Create mixed PRs - some actionable, some already processed
|
|
198
|
+
all_prs = []
|
|
199
|
+
|
|
200
|
+
# 5 actionable PRs
|
|
201
|
+
for i in range(5):
|
|
202
|
+
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}'
|
|
211
|
+
}
|
|
212
|
+
all_prs.append(pr)
|
|
213
|
+
|
|
214
|
+
# 3 already processed PRs (should be skipped)
|
|
215
|
+
for i in range(3):
|
|
216
|
+
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}'
|
|
225
|
+
}
|
|
226
|
+
# Pre-record as processed
|
|
227
|
+
self.monitor._record_pr_processing('test-repo', f'processed-branch-{i}', 2000 + i, f'abc123old{i:03d}')
|
|
228
|
+
all_prs.append(pr)
|
|
229
|
+
|
|
230
|
+
# RED: This will fail - no filter_and_process_prs method exists
|
|
231
|
+
processed_count = self.monitor.filter_and_process_prs(all_prs, target_actionable_count=10)
|
|
232
|
+
self.assertEqual(processed_count, 5) # Only actionable PRs counted
|
|
233
|
+
|
|
234
|
+
# Matrix 3: Eligible PR Detection
|
|
235
|
+
def test_matrix_filter_eligible_prs_from_mixed_list(self):
|
|
236
|
+
"""RED: Filter eligible PRs from mixed list → Should return only actionable ones"""
|
|
237
|
+
mixed_prs = [
|
|
238
|
+
# Actionable: Open, new commit
|
|
239
|
+
{
|
|
240
|
+
'number': 1001, 'state': 'open', 'isDraft': False,
|
|
241
|
+
'headRefOid': 'new123', 'repository': 'repo1',
|
|
242
|
+
'headRefName': 'branch1', 'repositoryFullName': 'org/repo1'
|
|
243
|
+
},
|
|
244
|
+
# Not actionable: Closed
|
|
245
|
+
{
|
|
246
|
+
'number': 1002, 'state': 'closed', 'isDraft': False,
|
|
247
|
+
'headRefOid': 'new456', 'repository': 'repo2',
|
|
248
|
+
'headRefName': 'branch2', 'repositoryFullName': 'org/repo2'
|
|
249
|
+
},
|
|
250
|
+
# Not actionable: Already processed
|
|
251
|
+
{
|
|
252
|
+
'number': 1003, 'state': 'open', 'isDraft': False,
|
|
253
|
+
'headRefOid': 'old789', 'repository': 'repo3',
|
|
254
|
+
'headRefName': 'branch3', 'repositoryFullName': 'org/repo3'
|
|
255
|
+
},
|
|
256
|
+
# Actionable: Draft but new commit
|
|
257
|
+
{
|
|
258
|
+
'number': 1004, 'state': 'open', 'isDraft': True,
|
|
259
|
+
'headRefOid': 'new999', 'repository': 'repo4',
|
|
260
|
+
'headRefName': 'branch4', 'repositoryFullName': 'org/repo4'
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
# Mark one as already processed
|
|
265
|
+
self.monitor._record_pr_processing('repo3', 'branch3', 1003, 'old789')
|
|
266
|
+
|
|
267
|
+
# RED: This will fail - no filter_eligible_prs method exists
|
|
268
|
+
eligible_prs = self.monitor.filter_eligible_prs(mixed_prs)
|
|
269
|
+
|
|
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]
|
|
273
|
+
self.assertIn(1001, actionable_numbers)
|
|
274
|
+
self.assertIn(1004, actionable_numbers)
|
|
275
|
+
self.assertNotIn(1002, actionable_numbers) # Closed
|
|
276
|
+
self.assertNotIn(1003, actionable_numbers) # Already processed
|
|
277
|
+
|
|
278
|
+
def test_matrix_find_5_eligible_prs_from_live_data(self):
|
|
279
|
+
"""RED: Find 5 eligible PRs from live GitHub data → Should return 5 actionable PRs"""
|
|
280
|
+
# Mock discover_open_prs to return test data instead of calling GitHub API
|
|
281
|
+
mock_prs = [
|
|
282
|
+
{"number": 1, "state": "open", "isDraft": False, "headRefOid": "abc123", "repository": "repo1", "headRefName": "feature1"},
|
|
283
|
+
{"number": 2, "state": "closed", "isDraft": False, "headRefOid": "def456", "repository": "repo2", "headRefName": "feature2"},
|
|
284
|
+
{"number": 3, "state": "open", "isDraft": False, "headRefOid": "ghi789", "repository": "repo3", "headRefName": "feature3"},
|
|
285
|
+
{"number": 4, "state": "open", "isDraft": True, "headRefOid": "jkl012", "repository": "repo4", "headRefName": "feature4"},
|
|
286
|
+
{"number": 5, "state": "open", "isDraft": False, "headRefOid": "mno345", "repository": "repo5", "headRefName": "feature5"},
|
|
287
|
+
{"number": 6, "state": "open", "isDraft": False, "headRefOid": "pqr678", "repository": "repo6", "headRefName": "feature6"},
|
|
288
|
+
{"number": 7, "state": "open", "isDraft": False, "headRefOid": "stu901", "repository": "repo7", "headRefName": "feature7"}
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
with patch.object(self.monitor, 'discover_open_prs', return_value=mock_prs):
|
|
292
|
+
eligible_prs = self.monitor.find_eligible_prs(limit=5)
|
|
293
|
+
self.assertEqual(len(eligible_prs), 5)
|
|
294
|
+
# All returned PRs should be actionable
|
|
295
|
+
for pr in eligible_prs:
|
|
296
|
+
self.assertTrue(self.monitor.is_pr_actionable(pr))
|
|
297
|
+
|
|
298
|
+
# Matrix 5: Comment Posting Return Values (Bug Fix Tests)
|
|
299
|
+
def test_comment_posting_returns_posted_on_success(self):
|
|
300
|
+
"""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:
|
|
307
|
+
|
|
308
|
+
# Setup: PR not skipped, no existing comment, successful command
|
|
309
|
+
mock_state.return_value = ('sha123', [])
|
|
310
|
+
mock_skip.return_value = False
|
|
311
|
+
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='')
|
|
314
|
+
|
|
315
|
+
pr_data = {
|
|
316
|
+
'repositoryFullName': 'org/repo',
|
|
317
|
+
'headRefName': 'feature'
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
|
|
321
|
+
self.assertEqual(result, 'posted')
|
|
322
|
+
mock_record.assert_called_once()
|
|
323
|
+
|
|
324
|
+
def test_comment_posting_returns_skipped_when_already_processed(self):
|
|
325
|
+
"""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:
|
|
328
|
+
|
|
329
|
+
# Setup: PR should be skipped
|
|
330
|
+
mock_state.return_value = ('sha123', [])
|
|
331
|
+
mock_skip.return_value = True
|
|
332
|
+
|
|
333
|
+
pr_data = {
|
|
334
|
+
'repositoryFullName': 'org/repo',
|
|
335
|
+
'headRefName': 'feature'
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
|
|
339
|
+
self.assertEqual(result, 'skipped')
|
|
340
|
+
|
|
341
|
+
def test_comment_posting_returns_skipped_when_comment_exists(self):
|
|
342
|
+
"""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:
|
|
346
|
+
|
|
347
|
+
# Setup: PR not skipped but has existing comment
|
|
348
|
+
mock_state.return_value = ('sha123', [])
|
|
349
|
+
mock_skip.return_value = False
|
|
350
|
+
mock_has_comment.return_value = True
|
|
351
|
+
|
|
352
|
+
pr_data = {
|
|
353
|
+
'repositoryFullName': 'org/repo',
|
|
354
|
+
'headRefName': 'feature'
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
|
|
358
|
+
self.assertEqual(result, 'skipped')
|
|
359
|
+
|
|
360
|
+
def test_comment_posting_returns_failed_on_subprocess_error(self):
|
|
361
|
+
"""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:
|
|
367
|
+
|
|
368
|
+
# Setup: PR not skipped, no existing comment, but command fails
|
|
369
|
+
mock_state.return_value = ('sha123', [])
|
|
370
|
+
mock_skip.return_value = False
|
|
371
|
+
mock_has_comment.return_value = False
|
|
372
|
+
mock_build_body.return_value = 'Test comment body'
|
|
373
|
+
mock_subprocess.side_effect = Exception('Command failed')
|
|
374
|
+
|
|
375
|
+
pr_data = {
|
|
376
|
+
'repositoryFullName': 'org/repo',
|
|
377
|
+
'headRefName': 'feature'
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
|
|
381
|
+
self.assertEqual(result, 'failed')
|
|
382
|
+
|
|
383
|
+
def test_comment_posting_skips_when_head_commit_from_codex(self):
|
|
384
|
+
"""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'}
|
|
396
|
+
mock_is_codex.return_value = True
|
|
397
|
+
|
|
398
|
+
pr_data = {
|
|
399
|
+
'repositoryFullName': 'org/repo',
|
|
400
|
+
'headRefName': 'feature',
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
result = self.monitor.post_codex_instruction_simple('org/repo', 456, pr_data)
|
|
404
|
+
|
|
405
|
+
self.assertEqual(result, 'skipped')
|
|
406
|
+
mock_is_codex.assert_called_once_with({'sha': 'sha123'})
|
|
407
|
+
mock_should_skip.assert_not_called()
|
|
408
|
+
mock_has_comment.assert_not_called()
|
|
409
|
+
mock_build_body.assert_not_called()
|
|
410
|
+
mock_subprocess.assert_not_called()
|
|
411
|
+
mock_record_processed.assert_not_called()
|
|
412
|
+
|
|
413
|
+
def test_process_pr_comment_only_returns_true_for_posted(self):
|
|
414
|
+
"""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:
|
|
416
|
+
|
|
417
|
+
pr_data = {'repositoryFullName': 'org/repo'}
|
|
418
|
+
|
|
419
|
+
# Test: Returns True only for 'posted'
|
|
420
|
+
mock_post.return_value = 'posted'
|
|
421
|
+
self.assertTrue(self.monitor._process_pr_comment('repo', 123, pr_data))
|
|
422
|
+
|
|
423
|
+
# Test: Returns False for 'skipped'
|
|
424
|
+
mock_post.return_value = 'skipped'
|
|
425
|
+
self.assertFalse(self.monitor._process_pr_comment('repo', 123, pr_data))
|
|
426
|
+
|
|
427
|
+
# Test: Returns False for 'failed'
|
|
428
|
+
mock_post.return_value = 'failed'
|
|
429
|
+
self.assertFalse(self.monitor._process_pr_comment('repo', 123, pr_data))
|
|
430
|
+
|
|
431
|
+
def test_comment_template_contains_all_ai_assistants(self):
|
|
432
|
+
"""GREEN: Comment template should mention all 4 AI assistants"""
|
|
433
|
+
pr_data = {
|
|
434
|
+
'title': 'Test PR',
|
|
435
|
+
'author': {'login': 'testuser'},
|
|
436
|
+
'headRefName': 'test-branch'
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
comment_body = self.monitor._build_codex_comment_body_simple(
|
|
440
|
+
'test/repo', 123, pr_data, 'abc12345'
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# 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")
|
|
448
|
+
|
|
449
|
+
# 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")
|
|
455
|
+
|
|
456
|
+
# Verify automation marker instructions are documented
|
|
457
|
+
self.assertIn(
|
|
458
|
+
self.monitor.CODEX_COMMIT_MESSAGE_MARKER,
|
|
459
|
+
comment_body,
|
|
460
|
+
"Comment should instruct Codex to include the commit message marker",
|
|
461
|
+
)
|
|
462
|
+
self.assertIn(
|
|
463
|
+
"<!-- codex-automation-commit:",
|
|
464
|
+
comment_body,
|
|
465
|
+
"Comment should remind Codex about the hidden commit marker",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if __name__ == '__main__':
|
|
470
|
+
# RED Phase: Run tests to confirm they FAIL
|
|
471
|
+
print("🔴 RED Phase: Running failing tests for PR filtering matrix")
|
|
472
|
+
print("Expected: ALL TESTS SHOULD FAIL (no implementation exists)")
|
|
473
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test PR targeting functionality for jleechanorg_pr_monitor - Codex Strategy Tests Only
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestPRTargeting(unittest.TestCase):
|
|
12
|
+
"""Test PR targeting functionality - Codex Strategy Only"""
|
|
13
|
+
|
|
14
|
+
def test_extract_commit_marker(self):
|
|
15
|
+
"""Commit markers can be parsed from Codex comments"""
|
|
16
|
+
monitor = JleechanorgPRMonitor()
|
|
17
|
+
test_comment = f"@codex @coderabbitai @copilot @cursor [AI automation] Test comment\n\n{monitor.CODEX_COMMIT_MARKER_PREFIX}abc123{monitor.CODEX_COMMIT_MARKER_SUFFIX}"
|
|
18
|
+
marker = monitor._extract_commit_marker(test_comment)
|
|
19
|
+
self.assertEqual(marker, "abc123")
|
|
20
|
+
|
|
21
|
+
def test_detect_pending_codex_commit(self):
|
|
22
|
+
"""Codex bot summary comments referencing head commit trigger pending detection."""
|
|
23
|
+
monitor = JleechanorgPRMonitor()
|
|
24
|
+
head_sha = "abcdef1234567890"
|
|
25
|
+
comments = [
|
|
26
|
+
{
|
|
27
|
+
"body": "**Summary**\nlink https://github.com/org/repo/blob/abcdef1234567890/path/file.py\n",
|
|
28
|
+
"author": {"login": "chatgpt-codex-connector[bot]"},
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
self.assertTrue(monitor._has_pending_codex_commit(comments, head_sha))
|
|
33
|
+
|
|
34
|
+
def test_pending_codex_commit_detects_short_sha_references(self):
|
|
35
|
+
"""Cursor Bugbot short SHA references should still count as pending commits."""
|
|
36
|
+
monitor = JleechanorgPRMonitor()
|
|
37
|
+
full_head_sha = "c279655d00dfcab5ac1a2fd9b0f6205ce5cbba12"
|
|
38
|
+
comments = [
|
|
39
|
+
{
|
|
40
|
+
"body": "Written by Cursor Bugbot for commit c279655. This will update automatically on new commits.",
|
|
41
|
+
"author": {"login": "chatgpt-codex-connector[bot]"},
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
self.assertTrue(monitor._has_pending_codex_commit(comments, full_head_sha))
|
|
46
|
+
|
|
47
|
+
def test_pending_codex_commit_ignores_short_head_sha(self):
|
|
48
|
+
"""Short head SHAs should not match longer Codex summary hashes."""
|
|
49
|
+
monitor = JleechanorgPRMonitor()
|
|
50
|
+
short_head_sha = "c279655"
|
|
51
|
+
comments = [
|
|
52
|
+
{
|
|
53
|
+
"body": "Written by Cursor Bugbot for commit c279655d00dfcab5ac1a2fd9b0f6205ce5cbba12.",
|
|
54
|
+
"author": {"login": "chatgpt-codex-connector[bot]"},
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
self.assertFalse(monitor._has_pending_codex_commit(comments, short_head_sha))
|
|
59
|
+
|
|
60
|
+
def test_pending_codex_commit_requires_codex_author(self):
|
|
61
|
+
"""Pending detection ignores non-Codex authors even if commit appears in comment."""
|
|
62
|
+
monitor = JleechanorgPRMonitor()
|
|
63
|
+
head_sha = "abcdef1234567890"
|
|
64
|
+
comments = [
|
|
65
|
+
{
|
|
66
|
+
"body": "Please review commit https://github.com/org/repo/commit/abcdef1234567890",
|
|
67
|
+
"author": {"login": "reviewer"},
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
self.assertFalse(monitor._has_pending_codex_commit(comments, head_sha))
|
|
72
|
+
|
|
73
|
+
def test_codex_comment_includes_detailed_execution_flow(self):
|
|
74
|
+
"""Automation comment should summarize the enforced execution flow with numbered steps."""
|
|
75
|
+
monitor = JleechanorgPRMonitor()
|
|
76
|
+
pr_data = {
|
|
77
|
+
"title": "Improve automation summary",
|
|
78
|
+
"author": {"login": "developer"},
|
|
79
|
+
"headRefName": "feature/automation-flow",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
comment_body = monitor._build_codex_comment_body_simple(
|
|
83
|
+
"jleechanorg/worldarchitect.ai",
|
|
84
|
+
42,
|
|
85
|
+
pr_data,
|
|
86
|
+
"abcdef1234567890",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.assertIn("**Summary (Execution Flow):**", comment_body)
|
|
90
|
+
self.assertIn("1. Review every outstanding PR comment", comment_body)
|
|
91
|
+
self.assertIn("5. Perform a final self-review", comment_body)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == '__main__':
|
|
95
|
+
unittest.main()
|