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.
- jleechanorg_pr_automation/STORAGE_STATE_TESTING_PROTOCOL.md +326 -0
- jleechanorg_pr_automation/__init__.py +64 -9
- jleechanorg_pr_automation/automation_safety_manager.py +306 -95
- jleechanorg_pr_automation/automation_safety_wrapper.py +13 -19
- jleechanorg_pr_automation/automation_utils.py +87 -65
- jleechanorg_pr_automation/check_codex_comment.py +7 -1
- jleechanorg_pr_automation/codex_branch_updater.py +21 -9
- jleechanorg_pr_automation/codex_config.py +70 -3
- jleechanorg_pr_automation/jleechanorg_pr_monitor.py +1954 -234
- jleechanorg_pr_automation/logging_utils.py +86 -0
- jleechanorg_pr_automation/openai_automation/__init__.py +3 -0
- jleechanorg_pr_automation/openai_automation/codex_github_mentions.py +1111 -0
- jleechanorg_pr_automation/openai_automation/debug_page_content.py +88 -0
- jleechanorg_pr_automation/openai_automation/oracle_cli.py +364 -0
- jleechanorg_pr_automation/openai_automation/test_auth_restoration.py +244 -0
- jleechanorg_pr_automation/openai_automation/test_codex_comprehensive.py +355 -0
- jleechanorg_pr_automation/openai_automation/test_codex_integration.py +254 -0
- jleechanorg_pr_automation/orchestrated_pr_runner.py +516 -0
- jleechanorg_pr_automation/tests/__init__.py +0 -0
- jleechanorg_pr_automation/tests/test_actionable_counting_matrix.py +84 -86
- jleechanorg_pr_automation/tests/test_attempt_limit_logic.py +124 -0
- jleechanorg_pr_automation/tests/test_automation_marker_functions.py +175 -0
- jleechanorg_pr_automation/tests/test_automation_over_running_reproduction.py +9 -11
- jleechanorg_pr_automation/tests/test_automation_safety_limits.py +91 -79
- jleechanorg_pr_automation/tests/test_automation_safety_manager_comprehensive.py +53 -53
- jleechanorg_pr_automation/tests/test_codex_actor_matching.py +1 -1
- jleechanorg_pr_automation/tests/test_fixpr_prompt.py +54 -0
- jleechanorg_pr_automation/tests/test_fixpr_return_value.py +140 -0
- jleechanorg_pr_automation/tests/test_graphql_error_handling.py +26 -26
- jleechanorg_pr_automation/tests/test_model_parameter.py +317 -0
- jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +697 -0
- jleechanorg_pr_automation/tests/test_packaging_integration.py +127 -0
- jleechanorg_pr_automation/tests/test_pr_filtering_matrix.py +246 -193
- jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +354 -0
- jleechanorg_pr_automation/tests/test_pr_targeting.py +102 -7
- jleechanorg_pr_automation/tests/test_version_consistency.py +51 -0
- jleechanorg_pr_automation/tests/test_workflow_specific_limits.py +202 -0
- jleechanorg_pr_automation/tests/test_workspace_dispatch_missing_dir.py +119 -0
- jleechanorg_pr_automation/utils.py +81 -56
- jleechanorg_pr_automation-0.2.45.dist-info/METADATA +864 -0
- jleechanorg_pr_automation-0.2.45.dist-info/RECORD +45 -0
- jleechanorg_pr_automation-0.1.1.dist-info/METADATA +0 -222
- jleechanorg_pr_automation-0.1.1.dist-info/RECORD +0 -23
- {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/WHEEL +0 -0
- {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/entry_points.txt +0 -0
- {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
|
|
15
|
-
from
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
111
|
-
"""
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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(
|
|
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
|
-
"""
|
|
232
|
+
"""Filter eligible PRs from mixed list skips drafts"""
|
|
237
233
|
mixed_prs = [
|
|
238
234
|
# Actionable: Open, new commit
|
|
239
235
|
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
248
|
+
"number": 1003, "state": "open", "isDraft": False,
|
|
249
|
+
"headRefOid": "old789", "repository": "repo3",
|
|
250
|
+
"headRefName": "branch3", "repositoryFullName": "org/repo3"
|
|
255
251
|
},
|
|
256
|
-
#
|
|
252
|
+
# Skipped: Draft even with new commit
|
|
257
253
|
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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(
|
|
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
|
|
271
|
-
self.assertEqual(len(eligible_prs),
|
|
272
|
-
actionable_numbers = [pr[
|
|
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,
|
|
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,
|
|
302
|
-
patch.object(self.monitor,
|
|
303
|
-
patch.object(self.monitor,
|
|
304
|
-
patch.object(self.monitor,
|
|
305
|
-
patch.object(self.monitor,
|
|
306
|
-
patch(
|
|
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 = (
|
|
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 =
|
|
313
|
-
mock_subprocess.return_value = Mock(returncode=0, stdout=
|
|
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
|
-
|
|
317
|
-
|
|
311
|
+
"repositoryFullName": "org/repo",
|
|
312
|
+
"headRefName": "feature"
|
|
318
313
|
}
|
|
319
314
|
|
|
320
|
-
result = self.monitor.post_codex_instruction_simple(
|
|
321
|
-
self.assertEqual(result,
|
|
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,
|
|
327
|
-
patch.object(self.monitor,
|
|
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 = (
|
|
325
|
+
mock_state.return_value = ("sha123", [])
|
|
331
326
|
mock_skip.return_value = True
|
|
332
327
|
|
|
333
328
|
pr_data = {
|
|
334
|
-
|
|
335
|
-
|
|
329
|
+
"repositoryFullName": "org/repo",
|
|
330
|
+
"headRefName": "feature"
|
|
336
331
|
}
|
|
337
332
|
|
|
338
|
-
result = self.monitor.post_codex_instruction_simple(
|
|
339
|
-
self.assertEqual(result,
|
|
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,
|
|
344
|
-
patch.object(self.monitor,
|
|
345
|
-
patch.object(self.monitor,
|
|
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 = (
|
|
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
|
-
|
|
354
|
-
|
|
348
|
+
"repositoryFullName": "org/repo",
|
|
349
|
+
"headRefName": "feature"
|
|
355
350
|
}
|
|
356
351
|
|
|
357
|
-
result = self.monitor.post_codex_instruction_simple(
|
|
358
|
-
self.assertEqual(result,
|
|
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,
|
|
363
|
-
patch.object(self.monitor,
|
|
364
|
-
patch.object(self.monitor,
|
|
365
|
-
patch.object(self.monitor,
|
|
366
|
-
patch(
|
|
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 = (
|
|
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 =
|
|
373
|
-
mock_subprocess.side_effect = Exception(
|
|
367
|
+
mock_build_body.return_value = "Test comment body"
|
|
368
|
+
mock_subprocess.side_effect = Exception("Command failed")
|
|
374
369
|
|
|
375
370
|
pr_data = {
|
|
376
|
-
|
|
377
|
-
|
|
371
|
+
"repositoryFullName": "org/repo",
|
|
372
|
+
"headRefName": "feature"
|
|
378
373
|
}
|
|
379
374
|
|
|
380
|
-
result = self.monitor.post_codex_instruction_simple(
|
|
381
|
-
self.assertEqual(result,
|
|
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,
|
|
386
|
-
patch.object(self.monitor,
|
|
387
|
-
patch.object(self.monitor,
|
|
388
|
-
patch.object(self.monitor,
|
|
389
|
-
patch.object(self.monitor,
|
|
390
|
-
patch.object(self.monitor,
|
|
391
|
-
patch.object(self.monitor,
|
|
392
|
-
patch(
|
|
393
|
-
|
|
394
|
-
mock_state.return_value = (
|
|
395
|
-
mock_head_details.return_value = {
|
|
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
|
-
|
|
400
|
-
|
|
394
|
+
"repositoryFullName": "org/repo",
|
|
395
|
+
"headRefName": "feature",
|
|
401
396
|
}
|
|
402
397
|
|
|
403
|
-
result = self.monitor.post_codex_instruction_simple(
|
|
398
|
+
result = self.monitor.post_codex_instruction_simple("org/repo", 456, pr_data)
|
|
404
399
|
|
|
405
|
-
self.assertEqual(result,
|
|
406
|
-
mock_is_codex.assert_called_once_with({
|
|
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(
|
|
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,
|
|
410
|
+
with patch.object(self.monitor, "post_codex_instruction_simple") as mock_post:
|
|
416
411
|
|
|
417
|
-
pr_data = {
|
|
412
|
+
pr_data = {"repositoryFullName": "org/repo"}
|
|
418
413
|
|
|
419
414
|
# Test: Returns True only for 'posted'
|
|
420
|
-
mock_post.return_value =
|
|
421
|
-
self.assertTrue(self.monitor._process_pr_comment(
|
|
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 =
|
|
425
|
-
self.assertFalse(self.monitor._process_pr_comment(
|
|
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 =
|
|
429
|
-
self.assertFalse(self.monitor._process_pr_comment(
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
435
|
+
"test/repo", 123, pr_data, "abc12345"
|
|
441
436
|
)
|
|
442
437
|
|
|
443
438
|
# Verify all 4 AI assistant mentions are present
|
|
444
|
-
self.assertIn(
|
|
445
|
-
self.assertIn(
|
|
446
|
-
self.assertIn(
|
|
447
|
-
self.assertIn(
|
|
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(
|
|
451
|
-
self.assertIn(
|
|
452
|
-
self.assertIn(
|
|
453
|
-
self.assertIn(
|
|
454
|
-
self.assertIn(
|
|
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__ ==
|
|
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)")
|