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
|
@@ -6,11 +6,9 @@ Test Matrix: Actionable PR counting should exclude skipped PRs and only count
|
|
|
6
6
|
PRs that actually get processed with comments.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import os
|
|
10
|
-
import unittest
|
|
11
9
|
import tempfile
|
|
12
|
-
|
|
13
|
-
from unittest.mock import
|
|
10
|
+
import unittest
|
|
11
|
+
from unittest.mock import patch
|
|
14
12
|
|
|
15
13
|
from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
|
|
16
14
|
|
|
@@ -21,7 +19,7 @@ class TestActionableCountingMatrix(unittest.TestCase):
|
|
|
21
19
|
def setUp(self):
|
|
22
20
|
"""Set up test environment"""
|
|
23
21
|
self.temp_dir = tempfile.mkdtemp()
|
|
24
|
-
self.monitor = JleechanorgPRMonitor()
|
|
22
|
+
self.monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
25
23
|
self.monitor.history_storage_path = self.temp_dir
|
|
26
24
|
|
|
27
25
|
def tearDown(self):
|
|
@@ -35,40 +33,40 @@ class TestActionableCountingMatrix(unittest.TestCase):
|
|
|
35
33
|
# Create a mix of PRs - some actionable, some should be skipped
|
|
36
34
|
mock_prs = [
|
|
37
35
|
# 3 actionable PRs (new commits)
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
{"number": 1001, "state": "open", "isDraft": False, "headRefOid": "new001",
|
|
37
|
+
"repository": "repo1", "headRefName": "feature1", "repositoryFullName": "org/repo1",
|
|
38
|
+
"title": "Actionable PR 1", "updatedAt": "2025-09-28T21:00:00Z"},
|
|
39
|
+
{"number": 1002, "state": "open", "isDraft": False, "headRefOid": "new002",
|
|
40
|
+
"repository": "repo2", "headRefName": "feature2", "repositoryFullName": "org/repo2",
|
|
41
|
+
"title": "Actionable PR 2", "updatedAt": "2025-09-28T20:59:00Z"},
|
|
42
|
+
{"number": 1003, "state": "open", "isDraft": False, "headRefOid": "new003",
|
|
43
|
+
"repository": "repo3", "headRefName": "feature3", "repositoryFullName": "org/repo3",
|
|
44
|
+
"title": "Actionable PR 3", "updatedAt": "2025-09-28T20:58:00Z"},
|
|
47
45
|
|
|
48
46
|
# 2 PRs that should be skipped (already processed)
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
{"number": 2001, "state": "open", "isDraft": False, "headRefOid": "old001",
|
|
48
|
+
"repository": "repo4", "headRefName": "processed1", "repositoryFullName": "org/repo4",
|
|
49
|
+
"title": "Already Processed PR 1", "updatedAt": "2025-09-28T20:57:00Z"},
|
|
50
|
+
{"number": 2002, "state": "open", "isDraft": False, "headRefOid": "old002",
|
|
51
|
+
"repository": "repo5", "headRefName": "processed2", "repositoryFullName": "org/repo5",
|
|
52
|
+
"title": "Already Processed PR 2", "updatedAt": "2025-09-28T20:56:00Z"}
|
|
55
53
|
]
|
|
56
54
|
|
|
57
55
|
# Pre-record the "processed" PRs as already handled
|
|
58
|
-
self.monitor._record_pr_processing(
|
|
59
|
-
self.monitor._record_pr_processing(
|
|
56
|
+
self.monitor._record_pr_processing("repo4", "processed1", 2001, "old001")
|
|
57
|
+
self.monitor._record_pr_processing("repo5", "processed2", 2002, "old002")
|
|
60
58
|
|
|
61
59
|
# Mock the PR discovery to return our test data
|
|
62
|
-
with patch.object(self.monitor,
|
|
63
|
-
with patch.object(self.monitor,
|
|
60
|
+
with patch.object(self.monitor, "discover_open_prs", return_value=mock_prs):
|
|
61
|
+
with patch.object(self.monitor, "_process_pr_comment", return_value=True) as mock_process:
|
|
64
62
|
|
|
65
63
|
# RED: This should fail - current implementation counts all PRs, not just actionable ones
|
|
66
64
|
result = self.monitor.run_monitoring_cycle_with_actionable_count(target_actionable_count=3)
|
|
67
65
|
|
|
68
66
|
# Should process exactly 3 actionable PRs, not counting the 2 skipped ones
|
|
69
|
-
self.assertEqual(result[
|
|
70
|
-
self.assertEqual(result[
|
|
71
|
-
self.assertEqual(result[
|
|
67
|
+
self.assertEqual(result["actionable_processed"], 3)
|
|
68
|
+
self.assertEqual(result["total_discovered"], 5)
|
|
69
|
+
self.assertEqual(result["skipped_count"], 2)
|
|
72
70
|
|
|
73
71
|
# Verify that _process_pr_comment was called exactly 3 times (only for actionable PRs)
|
|
74
72
|
self.assertEqual(mock_process.call_count, 3)
|
|
@@ -78,35 +76,35 @@ class TestActionableCountingMatrix(unittest.TestCase):
|
|
|
78
76
|
|
|
79
77
|
# Create only 2 actionable PRs, but set target to 5
|
|
80
78
|
mock_prs = [
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
{"number": 1001, "state": "open", "isDraft": False, "headRefOid": "new001",
|
|
80
|
+
"repository": "repo1", "headRefName": "feature1", "repositoryFullName": "org/repo1",
|
|
81
|
+
"title": "Actionable PR 1", "updatedAt": "2025-09-28T21:00:00Z"},
|
|
82
|
+
{"number": 1002, "state": "open", "isDraft": False, "headRefOid": "new002",
|
|
83
|
+
"repository": "repo2", "headRefName": "feature2", "repositoryFullName": "org/repo2",
|
|
84
|
+
"title": "Actionable PR 2", "updatedAt": "2025-09-28T20:59:00Z"},
|
|
87
85
|
|
|
88
86
|
# 3 closed/processed PRs (not actionable)
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
{"number": 2001, "state": "closed", "isDraft": False, "headRefOid": "any001",
|
|
88
|
+
"repository": "repo3", "headRefName": "closed1", "repositoryFullName": "org/repo3",
|
|
89
|
+
"title": "Closed PR", "updatedAt": "2025-09-28T20:58:00Z"},
|
|
90
|
+
{"number": 2002, "state": "open", "isDraft": False, "headRefOid": "old002",
|
|
91
|
+
"repository": "repo4", "headRefName": "processed2", "repositoryFullName": "org/repo4",
|
|
92
|
+
"title": "Processed PR", "updatedAt": "2025-09-28T20:57:00Z"}
|
|
95
93
|
]
|
|
96
94
|
|
|
97
95
|
# Pre-record one as processed
|
|
98
|
-
self.monitor._record_pr_processing(
|
|
96
|
+
self.monitor._record_pr_processing("repo4", "processed2", 2002, "old002")
|
|
99
97
|
|
|
100
|
-
with patch.object(self.monitor,
|
|
101
|
-
with patch.object(self.monitor,
|
|
98
|
+
with patch.object(self.monitor, "discover_open_prs", return_value=mock_prs):
|
|
99
|
+
with patch.object(self.monitor, "_process_pr_comment", return_value=True) as mock_process:
|
|
102
100
|
|
|
103
101
|
# RED: This should fail - method doesn't exist yet
|
|
104
102
|
result = self.monitor.run_monitoring_cycle_with_actionable_count(target_actionable_count=5)
|
|
105
103
|
|
|
106
104
|
# Should process only the 2 available actionable PRs
|
|
107
|
-
self.assertEqual(result[
|
|
108
|
-
self.assertEqual(result[
|
|
109
|
-
self.assertEqual(result[
|
|
105
|
+
self.assertEqual(result["actionable_processed"], 2)
|
|
106
|
+
self.assertEqual(result["total_discovered"], 4)
|
|
107
|
+
self.assertEqual(result["skipped_count"], 2) # 1 closed + 1 processed
|
|
110
108
|
|
|
111
109
|
# Verify processing was called only for actionable PRs
|
|
112
110
|
self.assertEqual(mock_process.call_count, 2)
|
|
@@ -117,30 +115,30 @@ class TestActionableCountingMatrix(unittest.TestCase):
|
|
|
117
115
|
# Create only non-actionable PRs
|
|
118
116
|
mock_prs = [
|
|
119
117
|
# All closed or already processed
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
{"number": 2001, "state": "closed", "isDraft": False, "headRefOid": "any001",
|
|
119
|
+
"repository": "repo1", "headRefName": "closed1", "repositoryFullName": "org/repo1",
|
|
120
|
+
"title": "Closed PR 1", "updatedAt": "2025-09-28T21:00:00Z"},
|
|
121
|
+
{"number": 2002, "state": "closed", "isDraft": False, "headRefOid": "any002",
|
|
122
|
+
"repository": "repo2", "headRefName": "closed2", "repositoryFullName": "org/repo2",
|
|
123
|
+
"title": "Closed PR 2", "updatedAt": "2025-09-28T20:59:00Z"},
|
|
124
|
+
{"number": 2003, "state": "open", "isDraft": False, "headRefOid": "old003",
|
|
125
|
+
"repository": "repo3", "headRefName": "processed3", "repositoryFullName": "org/repo3",
|
|
126
|
+
"title": "Processed PR", "updatedAt": "2025-09-28T20:58:00Z"}
|
|
129
127
|
]
|
|
130
128
|
|
|
131
129
|
# Mark the open one as already processed
|
|
132
|
-
self.monitor._record_pr_processing(
|
|
130
|
+
self.monitor._record_pr_processing("repo3", "processed3", 2003, "old003")
|
|
133
131
|
|
|
134
|
-
with patch.object(self.monitor,
|
|
135
|
-
with patch.object(self.monitor,
|
|
132
|
+
with patch.object(self.monitor, "discover_open_prs", return_value=mock_prs):
|
|
133
|
+
with patch.object(self.monitor, "_process_pr_comment", return_value=True) as mock_process:
|
|
136
134
|
|
|
137
135
|
# RED: This should fail - method doesn't exist yet
|
|
138
136
|
result = self.monitor.run_monitoring_cycle_with_actionable_count(target_actionable_count=10)
|
|
139
137
|
|
|
140
138
|
# Should process 0 PRs
|
|
141
|
-
self.assertEqual(result[
|
|
142
|
-
self.assertEqual(result[
|
|
143
|
-
self.assertEqual(result[
|
|
139
|
+
self.assertEqual(result["actionable_processed"], 0)
|
|
140
|
+
self.assertEqual(result["total_discovered"], 3)
|
|
141
|
+
self.assertEqual(result["skipped_count"], 3) # All skipped
|
|
144
142
|
|
|
145
143
|
# Verify no processing was attempted
|
|
146
144
|
self.assertEqual(mock_process.call_count, 0)
|
|
@@ -149,15 +147,15 @@ class TestActionableCountingMatrix(unittest.TestCase):
|
|
|
149
147
|
"""RED: Actionable counter should only count PRs that successfully get processed"""
|
|
150
148
|
|
|
151
149
|
mock_prs = [
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
150
|
+
{"number": 1001, "state": "open", "isDraft": False, "headRefOid": "new001",
|
|
151
|
+
"repository": "repo1", "headRefName": "feature1", "repositoryFullName": "org/repo1",
|
|
152
|
+
"title": "Success PR", "updatedAt": "2025-09-28T21:00:00Z"},
|
|
153
|
+
{"number": 1002, "state": "open", "isDraft": False, "headRefOid": "new002",
|
|
154
|
+
"repository": "repo2", "headRefName": "feature2", "repositoryFullName": "org/repo2",
|
|
155
|
+
"title": "Failure PR", "updatedAt": "2025-09-28T20:59:00Z"},
|
|
156
|
+
{"number": 1003, "state": "open", "isDraft": False, "headRefOid": "new003",
|
|
157
|
+
"repository": "repo3", "headRefName": "feature3", "repositoryFullName": "org/repo3",
|
|
158
|
+
"title": "Success PR 2", "updatedAt": "2025-09-28T20:58:00Z"}
|
|
161
159
|
]
|
|
162
160
|
|
|
163
161
|
def mock_process_side_effect(repo_name, pr_number, pr_data):
|
|
@@ -166,16 +164,16 @@ class TestActionableCountingMatrix(unittest.TestCase):
|
|
|
166
164
|
return False # Processing failed
|
|
167
165
|
return True # Processing succeeded
|
|
168
166
|
|
|
169
|
-
with patch.object(self.monitor,
|
|
170
|
-
with patch.object(self.monitor,
|
|
167
|
+
with patch.object(self.monitor, "discover_open_prs", return_value=mock_prs):
|
|
168
|
+
with patch.object(self.monitor, "_process_pr_comment", side_effect=mock_process_side_effect) as mock_process:
|
|
171
169
|
|
|
172
170
|
# RED: This should fail - method doesn't exist yet
|
|
173
171
|
result = self.monitor.run_monitoring_cycle_with_actionable_count(target_actionable_count=10)
|
|
174
172
|
|
|
175
173
|
# Should count only successful processing (2 out of 3 attempts)
|
|
176
|
-
self.assertEqual(result[
|
|
177
|
-
self.assertEqual(result[
|
|
178
|
-
self.assertEqual(result[
|
|
174
|
+
self.assertEqual(result["actionable_processed"], 2)
|
|
175
|
+
self.assertEqual(result["total_discovered"], 3)
|
|
176
|
+
self.assertEqual(result["processing_failures"], 1)
|
|
179
177
|
|
|
180
178
|
# Verify all 3 were attempted
|
|
181
179
|
self.assertEqual(mock_process.call_count, 3)
|
|
@@ -186,35 +184,35 @@ class TestActionableCountingMatrix(unittest.TestCase):
|
|
|
186
184
|
mock_prs = [
|
|
187
185
|
# Create 15 total PRs, but only 8 should be actionable
|
|
188
186
|
*[
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
{"number": 1000 + i, "state": "open", "isDraft": False, "headRefOid": f"new{i:03d}",
|
|
188
|
+
"repository": f"repo{i}", "headRefName": f"feature{i}", "repositoryFullName": f"org/repo{i}",
|
|
189
|
+
"title": f"Actionable PR {i}", "updatedAt": f"2025-09-28T{21-i//10}:{59-(i%10)*5}:00Z"}
|
|
192
190
|
for i in range(8) # 8 actionable PRs
|
|
193
191
|
],
|
|
194
192
|
*[
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
{"number": 2000 + i, "state": "closed", "isDraft": False, "headRefOid": f"any{i:03d}",
|
|
194
|
+
"repository": f"closed_repo{i}", "headRefName": f"closed{i}", "repositoryFullName": f"org/closed_repo{i}",
|
|
195
|
+
"title": f"Closed PR {i}", "updatedAt": f"2025-09-28T20:{50-i}:00Z"}
|
|
198
196
|
for i in range(7) # 7 closed PRs (not actionable)
|
|
199
197
|
]
|
|
200
198
|
]
|
|
201
199
|
|
|
202
|
-
with patch.object(self.monitor,
|
|
203
|
-
with patch.object(self.monitor,
|
|
200
|
+
with patch.object(self.monitor, "discover_open_prs", return_value=mock_prs):
|
|
201
|
+
with patch.object(self.monitor, "_process_pr_comment", return_value=True) as mock_process:
|
|
204
202
|
|
|
205
203
|
# RED: This should fail - enhanced method doesn't exist
|
|
206
204
|
# Should process exactly 5 actionable PRs, ignoring the 7 closed ones
|
|
207
205
|
result = self.monitor.run_monitoring_cycle_with_actionable_count(target_actionable_count=5)
|
|
208
206
|
|
|
209
|
-
self.assertEqual(result[
|
|
210
|
-
self.assertEqual(result[
|
|
211
|
-
self.assertEqual(result[
|
|
207
|
+
self.assertEqual(result["actionable_processed"], 5)
|
|
208
|
+
self.assertEqual(result["total_discovered"], 15)
|
|
209
|
+
self.assertEqual(result["skipped_count"], 7) # Closed PRs
|
|
212
210
|
|
|
213
211
|
# Should have attempted processing exactly 5 times
|
|
214
212
|
self.assertEqual(mock_process.call_count, 5)
|
|
215
213
|
|
|
216
214
|
|
|
217
|
-
if __name__ ==
|
|
215
|
+
if __name__ == "__main__":
|
|
218
216
|
# RED Phase: Run tests to confirm they FAIL
|
|
219
217
|
print("🔴 RED Phase: Running failing tests for actionable PR counting")
|
|
220
218
|
print("Expected: ALL TESTS SHOULD FAIL (no implementation exists)")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for per-PR attempt limit logic
|
|
3
|
+
|
|
4
|
+
NEW BEHAVIOR: Counts ALL attempts (success + failure) against the limit.
|
|
5
|
+
This enables iterative improvements by allowing multiple successful runs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import tempfile
|
|
12
|
+
import unittest
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from jleechanorg_pr_automation.automation_safety_manager import AutomationSafetyManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestAttemptLimitLogic(unittest.TestCase):
|
|
19
|
+
"""Test that attempt limits count ALL attempts (success + failure)"""
|
|
20
|
+
|
|
21
|
+
def setUp(self):
|
|
22
|
+
"""Set up test environment with temporary directory"""
|
|
23
|
+
self.test_dir = tempfile.mkdtemp()
|
|
24
|
+
self.safety_manager = AutomationSafetyManager(data_dir=self.test_dir)
|
|
25
|
+
# Note: Default pr_limit is now 50
|
|
26
|
+
|
|
27
|
+
def tearDown(self):
|
|
28
|
+
"""Clean up test directory"""
|
|
29
|
+
shutil.rmtree(self.test_dir, ignore_errors=True)
|
|
30
|
+
|
|
31
|
+
def test_all_attempts_count_against_limit(self):
|
|
32
|
+
"""All attempts (success + failure) count against pr_limit"""
|
|
33
|
+
# Create 50 successful attempts (exactly at pr_limit=50 default)
|
|
34
|
+
pr_attempts = {
|
|
35
|
+
"r=test/repo||p=123||b=main": [
|
|
36
|
+
{"result": "success", "timestamp": f"2026-01-{i:02d}T12:00:00"}
|
|
37
|
+
for i in range(1, 51)
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
attempts_file = os.path.join(self.test_dir, "pr_attempts.json")
|
|
42
|
+
with open(attempts_file, "w") as f:
|
|
43
|
+
json.dump(pr_attempts, f)
|
|
44
|
+
|
|
45
|
+
# Should be blocked (50 attempts = limit)
|
|
46
|
+
can_process = self.safety_manager.can_process_pr(123, repo="test/repo", branch="main")
|
|
47
|
+
self.assertFalse(can_process, "Should block processing at 50 total attempts")
|
|
48
|
+
|
|
49
|
+
def test_failure_limit_blocks_processing(self):
|
|
50
|
+
"""50 failures should block processing (pr_limit=50)"""
|
|
51
|
+
# Create 50 failed attempts (exactly at limit)
|
|
52
|
+
pr_attempts = {
|
|
53
|
+
"r=test/repo||p=456||b=main": [
|
|
54
|
+
{"result": "failure", "timestamp": f"2026-01-{i:02d}T12:00:00"}
|
|
55
|
+
for i in range(1, 51)
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
attempts_file = os.path.join(self.test_dir, "pr_attempts.json")
|
|
60
|
+
with open(attempts_file, "w") as f:
|
|
61
|
+
json.dump(pr_attempts, f)
|
|
62
|
+
|
|
63
|
+
# Should be blocked (50 failures = limit)
|
|
64
|
+
can_process = self.safety_manager.can_process_pr(456, repo="test/repo", branch="main")
|
|
65
|
+
self.assertFalse(can_process, "Should block processing with 50 failed attempts")
|
|
66
|
+
|
|
67
|
+
def test_mixed_attempts_count_all(self):
|
|
68
|
+
"""Mixed success/failure attempts all count toward limit"""
|
|
69
|
+
# Create 50 total attempts: 30 successes + 20 failures
|
|
70
|
+
pr_attempts = {
|
|
71
|
+
"r=test/repo||p=789||b=main": [
|
|
72
|
+
{"result": "success", "timestamp": f"2026-01-{i:02d}T12:00:00"}
|
|
73
|
+
for i in range(1, 31)
|
|
74
|
+
] + [
|
|
75
|
+
{"result": "failure", "timestamp": f"2026-01-{i:02d}T13:00:00"}
|
|
76
|
+
for i in range(31, 51)
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
attempts_file = os.path.join(self.test_dir, "pr_attempts.json")
|
|
81
|
+
with open(attempts_file, "w") as f:
|
|
82
|
+
json.dump(pr_attempts, f)
|
|
83
|
+
|
|
84
|
+
# Should be blocked (50 total attempts = limit)
|
|
85
|
+
can_process = self.safety_manager.can_process_pr(789, repo="test/repo", branch="main")
|
|
86
|
+
self.assertFalse(can_process, "Should block processing with 50 total attempts")
|
|
87
|
+
|
|
88
|
+
def test_under_limit_allows_processing(self):
|
|
89
|
+
"""Under the limit (49 attempts) should allow processing"""
|
|
90
|
+
# Create 49 attempts (1 under limit)
|
|
91
|
+
pr_attempts = {
|
|
92
|
+
"r=test/repo||p=999||b=main": [
|
|
93
|
+
{"result": "success", "timestamp": f"2026-01-{i:02d}T12:00:00"}
|
|
94
|
+
for i in range(1, 30)
|
|
95
|
+
] + [
|
|
96
|
+
{"result": "failure", "timestamp": f"2026-01-{i:02d}T13:00:00"}
|
|
97
|
+
for i in range(30, 50)
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
attempts_file = os.path.join(self.test_dir, "pr_attempts.json")
|
|
102
|
+
with open(attempts_file, "w") as f:
|
|
103
|
+
json.dump(pr_attempts, f)
|
|
104
|
+
|
|
105
|
+
# Should allow processing (49 < 50 limit)
|
|
106
|
+
can_process = self.safety_manager.can_process_pr(999, repo="test/repo", branch="main")
|
|
107
|
+
self.assertTrue(can_process, "Should allow processing with 49 total attempts")
|
|
108
|
+
|
|
109
|
+
def test_no_attempts_allows_processing(self):
|
|
110
|
+
"""No attempts should allow processing"""
|
|
111
|
+
# Empty attempts
|
|
112
|
+
pr_attempts = {}
|
|
113
|
+
|
|
114
|
+
attempts_file = os.path.join(self.test_dir, "pr_attempts.json")
|
|
115
|
+
with open(attempts_file, "w") as f:
|
|
116
|
+
json.dump(pr_attempts, f)
|
|
117
|
+
|
|
118
|
+
# Should allow processing (0 attempts)
|
|
119
|
+
can_process = self.safety_manager.can_process_pr(111, repo="test/repo", branch="main")
|
|
120
|
+
self.assertTrue(can_process, "Should allow processing with 0 attempts")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
unittest.main()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Unit tests for automation marker generation and parsing functions.
|
|
4
|
+
|
|
5
|
+
Tests build_automation_marker() and parse_automation_marker() helper functions
|
|
6
|
+
added to support enhanced automation markers with workflow:agent:commit format.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import unittest
|
|
10
|
+
|
|
11
|
+
from jleechanorg_pr_automation.codex_config import (
|
|
12
|
+
FIX_COMMENT_RUN_MARKER_PREFIX,
|
|
13
|
+
FIXPR_MARKER_PREFIX,
|
|
14
|
+
build_automation_marker,
|
|
15
|
+
parse_automation_marker,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestBuildAutomationMarker(unittest.TestCase):
|
|
20
|
+
"""Test build_automation_marker() function"""
|
|
21
|
+
|
|
22
|
+
def test_basic_marker_generation(self):
|
|
23
|
+
"""Test basic marker generation with all parameters"""
|
|
24
|
+
marker = build_automation_marker("fix-comment-run", "gemini", "abc1234")
|
|
25
|
+
expected = "<!-- fix-comment-run-automation-commit:gemini:abc1234-->"
|
|
26
|
+
self.assertEqual(marker, expected)
|
|
27
|
+
|
|
28
|
+
def test_fixpr_workflow(self):
|
|
29
|
+
"""Test marker generation for fixpr workflow"""
|
|
30
|
+
marker = build_automation_marker("fixpr-run", "codex", "def5678")
|
|
31
|
+
expected = "<!-- fixpr-run-automation-commit:codex:def5678-->"
|
|
32
|
+
self.assertEqual(marker, expected)
|
|
33
|
+
|
|
34
|
+
def test_different_agents(self):
|
|
35
|
+
"""Test marker generation with different agent names"""
|
|
36
|
+
agents = ["gemini", "codex", "claude"]
|
|
37
|
+
for agent in agents:
|
|
38
|
+
marker = build_automation_marker("fix-comment-run", agent, "sha123")
|
|
39
|
+
self.assertIn(f":{agent}:", marker)
|
|
40
|
+
self.assertTrue(marker.startswith("<!-- fix-comment-run-automation-commit:"))
|
|
41
|
+
self.assertTrue(marker.endswith("-->"))
|
|
42
|
+
|
|
43
|
+
def test_short_commit_sha(self):
|
|
44
|
+
"""Test marker generation with short commit SHA"""
|
|
45
|
+
marker = build_automation_marker("fix-comment-run", "gemini", "abc")
|
|
46
|
+
expected = "<!-- fix-comment-run-automation-commit:gemini:abc-->"
|
|
47
|
+
self.assertEqual(marker, expected)
|
|
48
|
+
|
|
49
|
+
def test_long_commit_sha(self):
|
|
50
|
+
"""Test marker generation with full 40-character commit SHA"""
|
|
51
|
+
full_sha = "a" * 40
|
|
52
|
+
marker = build_automation_marker("fix-comment-run", "gemini", full_sha)
|
|
53
|
+
expected = f"<!-- fix-comment-run-automation-commit:gemini:{full_sha}-->"
|
|
54
|
+
self.assertEqual(marker, expected)
|
|
55
|
+
|
|
56
|
+
def test_unknown_commit_fallback(self):
|
|
57
|
+
"""Test marker generation with 'unknown' commit value"""
|
|
58
|
+
marker = build_automation_marker("fix-comment-run", "gemini", "unknown")
|
|
59
|
+
expected = "<!-- fix-comment-run-automation-commit:gemini:unknown-->"
|
|
60
|
+
self.assertEqual(marker, expected)
|
|
61
|
+
|
|
62
|
+
def test_marker_format_consistency(self):
|
|
63
|
+
"""Test that marker format matches prefix constants"""
|
|
64
|
+
# Fix-comment marker should match FIX_COMMENT_RUN_MARKER_PREFIX
|
|
65
|
+
marker = build_automation_marker("fix-comment-run", "gemini", "abc123")
|
|
66
|
+
self.assertTrue(marker.startswith(FIX_COMMENT_RUN_MARKER_PREFIX))
|
|
67
|
+
|
|
68
|
+
# FixPR marker should match FIXPR_MARKER_PREFIX
|
|
69
|
+
marker = build_automation_marker("fixpr-run", "codex", "def456")
|
|
70
|
+
self.assertTrue(marker.startswith(FIXPR_MARKER_PREFIX))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestParseAutomationMarker(unittest.TestCase):
|
|
74
|
+
"""Test parse_automation_marker() function"""
|
|
75
|
+
|
|
76
|
+
def test_parse_new_format_with_agent(self):
|
|
77
|
+
"""Test parsing new format marker with agent"""
|
|
78
|
+
marker = "<!-- fix-comment-run-automation-commit:gemini:abc123-->"
|
|
79
|
+
result = parse_automation_marker(marker)
|
|
80
|
+
|
|
81
|
+
self.assertIsNotNone(result)
|
|
82
|
+
self.assertEqual(result["workflow"], "fix-comment-run")
|
|
83
|
+
self.assertEqual(result["agent"], "gemini")
|
|
84
|
+
self.assertEqual(result["commit"], "abc123")
|
|
85
|
+
|
|
86
|
+
def test_parse_fixpr_marker(self):
|
|
87
|
+
"""Test parsing fixpr-run marker"""
|
|
88
|
+
marker = "<!-- fixpr-run-automation-commit:codex:def456-->"
|
|
89
|
+
result = parse_automation_marker(marker)
|
|
90
|
+
|
|
91
|
+
self.assertIsNotNone(result)
|
|
92
|
+
self.assertEqual(result["workflow"], "fixpr-run")
|
|
93
|
+
self.assertEqual(result["agent"], "codex")
|
|
94
|
+
self.assertEqual(result["commit"], "def456")
|
|
95
|
+
|
|
96
|
+
def test_parse_legacy_format_without_agent(self):
|
|
97
|
+
"""Test parsing legacy format marker without agent"""
|
|
98
|
+
marker = "<!-- fix-comment-automation-commit:abc123-->"
|
|
99
|
+
result = parse_automation_marker(marker)
|
|
100
|
+
|
|
101
|
+
self.assertIsNotNone(result)
|
|
102
|
+
self.assertEqual(result["workflow"], "fix-comment")
|
|
103
|
+
self.assertEqual(result["agent"], "unknown")
|
|
104
|
+
self.assertEqual(result["commit"], "abc123")
|
|
105
|
+
|
|
106
|
+
def test_parse_legacy_fixpr_marker(self):
|
|
107
|
+
"""Test parsing legacy fixpr marker without agent"""
|
|
108
|
+
marker = "<!-- fixpr-automation-commit:ghi789-->"
|
|
109
|
+
result = parse_automation_marker(marker)
|
|
110
|
+
|
|
111
|
+
self.assertIsNotNone(result)
|
|
112
|
+
self.assertEqual(result["workflow"], "fixpr")
|
|
113
|
+
self.assertEqual(result["agent"], "unknown")
|
|
114
|
+
self.assertEqual(result["commit"], "ghi789")
|
|
115
|
+
|
|
116
|
+
def test_parse_invalid_marker_missing_html_comment(self):
|
|
117
|
+
"""Test parsing invalid marker missing HTML comment markers"""
|
|
118
|
+
marker = "fix-comment-run-automation-commit:gemini:abc123"
|
|
119
|
+
result = parse_automation_marker(marker)
|
|
120
|
+
self.assertIsNone(result)
|
|
121
|
+
|
|
122
|
+
def test_parse_invalid_marker_wrong_format(self):
|
|
123
|
+
"""Test parsing invalid marker with wrong format"""
|
|
124
|
+
marker = "<!-- some random comment -->"
|
|
125
|
+
result = parse_automation_marker(marker)
|
|
126
|
+
self.assertIsNone(result)
|
|
127
|
+
|
|
128
|
+
def test_parse_invalid_marker_too_many_colons(self):
|
|
129
|
+
"""Test parsing marker with too many colons"""
|
|
130
|
+
marker = "<!-- fix-comment-run-automation-commit:gemini:abc123:extra-->"
|
|
131
|
+
result = parse_automation_marker(marker)
|
|
132
|
+
# Should return None for invalid format (3 colons instead of 2)
|
|
133
|
+
self.assertIsNone(result)
|
|
134
|
+
|
|
135
|
+
def test_parse_marker_with_whitespace(self):
|
|
136
|
+
"""Test parsing marker with extra whitespace"""
|
|
137
|
+
marker = "<!-- fix-comment-run-automation-commit:gemini:abc123 -->"
|
|
138
|
+
result = parse_automation_marker(marker)
|
|
139
|
+
|
|
140
|
+
# Parser should handle trailing whitespace gracefully
|
|
141
|
+
self.assertIsNotNone(result)
|
|
142
|
+
self.assertEqual(result["workflow"], "fix-comment-run")
|
|
143
|
+
|
|
144
|
+
def test_roundtrip_consistency(self):
|
|
145
|
+
"""Test that building and parsing a marker gives consistent results"""
|
|
146
|
+
workflow = "fix-comment-run"
|
|
147
|
+
agent = "gemini"
|
|
148
|
+
commit = "abc1234"
|
|
149
|
+
|
|
150
|
+
# Build marker
|
|
151
|
+
marker = build_automation_marker(workflow, agent, commit)
|
|
152
|
+
|
|
153
|
+
# Parse it back
|
|
154
|
+
result = parse_automation_marker(marker)
|
|
155
|
+
|
|
156
|
+
# Verify roundtrip consistency
|
|
157
|
+
self.assertIsNotNone(result)
|
|
158
|
+
self.assertEqual(result["workflow"], workflow)
|
|
159
|
+
self.assertEqual(result["agent"], agent)
|
|
160
|
+
self.assertEqual(result["commit"], commit)
|
|
161
|
+
|
|
162
|
+
def test_parse_with_different_workflows(self):
|
|
163
|
+
"""Test parsing markers with different workflow types"""
|
|
164
|
+
workflows = ["fix-comment-run", "fixpr-run", "codex", "pr-automation"]
|
|
165
|
+
|
|
166
|
+
for workflow in workflows:
|
|
167
|
+
marker = build_automation_marker(workflow, "gemini", "abc123")
|
|
168
|
+
result = parse_automation_marker(marker)
|
|
169
|
+
|
|
170
|
+
self.assertIsNotNone(result)
|
|
171
|
+
self.assertEqual(result["workflow"], workflow)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
unittest.main()
|