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,13 +8,11 @@ This test reproduces the exact issue discovered:
|
|
|
8
8
|
- No default manual override (should require explicit --manual_override)
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import unittest
|
|
12
|
-
import tempfile
|
|
13
|
-
import os
|
|
14
|
-
import shutil
|
|
15
11
|
import argparse
|
|
16
|
-
import
|
|
17
|
-
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
import unittest
|
|
15
|
+
from datetime import datetime
|
|
18
16
|
|
|
19
17
|
from jleechanorg_pr_automation.automation_safety_manager import AutomationSafetyManager
|
|
20
18
|
|
|
@@ -81,12 +79,12 @@ class TestAutomationOverRunningReproduction(unittest.TestCase):
|
|
|
81
79
|
# Test that we can use --manual_override
|
|
82
80
|
# Parse the new arguments
|
|
83
81
|
parser = argparse.ArgumentParser()
|
|
84
|
-
parser.add_argument(
|
|
82
|
+
parser.add_argument("--manual_override", type=str, help="Correct command")
|
|
85
83
|
|
|
86
84
|
# The --manual_override command should work
|
|
87
|
-
args = parser.parse_args([
|
|
85
|
+
args = parser.parse_args(["--manual_override", "test@example.com"])
|
|
88
86
|
self.assertIsNotNone(args.manual_override, "--manual_override command works")
|
|
89
|
-
self.assertEqual(args.manual_override,
|
|
87
|
+
self.assertEqual(args.manual_override, "test@example.com")
|
|
90
88
|
|
|
91
89
|
# Test that --approve should no longer exist in the real CLI
|
|
92
90
|
# (This test verifies our refactoring was successful)
|
|
@@ -124,7 +122,7 @@ class TestAutomationOverRunningReproduction(unittest.TestCase):
|
|
|
124
122
|
This test documents the issue but doesn't enforce rate limiting yet.
|
|
125
123
|
"""
|
|
126
124
|
# Document that rate limiting doesn't exist (future enhancement)
|
|
127
|
-
self.assertFalse(hasattr(self.manager,
|
|
125
|
+
self.assertFalse(hasattr(self.manager, "check_rate_limit"),
|
|
128
126
|
"Rate limiting could be added as future enhancement")
|
|
129
127
|
|
|
130
128
|
# Document the excessive rate that occurred (historical reference)
|
|
@@ -143,7 +141,7 @@ class TestAutomationOverRunningReproduction(unittest.TestCase):
|
|
|
143
141
|
"New limits (100 max) would have prevented 346 runs")
|
|
144
142
|
|
|
145
143
|
|
|
146
|
-
if __name__ ==
|
|
144
|
+
if __name__ == "__main__":
|
|
147
145
|
print("🟢 GREEN PHASE: Running tests that now PASS after fixes")
|
|
148
146
|
print("✅ Automation over-running issue RESOLVED")
|
|
149
147
|
print("")
|
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
Test-Driven Development for PR Automation Safety Limits
|
|
4
4
|
|
|
5
5
|
RED Phase: All tests should FAIL initially
|
|
6
|
-
- PR attempt limits (max
|
|
6
|
+
- PR attempt limits (max 50 per PR - counts ALL attempts, not just failures)
|
|
7
7
|
- Global run limits (max 50 total)
|
|
8
8
|
- Manual approval requirement
|
|
9
|
+
|
|
10
|
+
NEW BEHAVIOR: Counts ALL attempts (success + failure) against the limit.
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
|
-
import os
|
|
12
|
-
import unittest
|
|
13
|
-
import tempfile
|
|
14
13
|
import json
|
|
14
|
+
import os
|
|
15
15
|
import shutil
|
|
16
|
+
import tempfile
|
|
17
|
+
import unittest
|
|
16
18
|
from datetime import datetime, timedelta
|
|
17
19
|
from pathlib import Path
|
|
18
|
-
from unittest.mock import patch
|
|
20
|
+
from unittest.mock import patch
|
|
19
21
|
|
|
20
22
|
from jleechanorg_pr_automation.automation_safety_manager import AutomationSafetyManager
|
|
21
23
|
|
|
@@ -30,22 +32,22 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
30
32
|
self.global_runs_file = os.path.join(self.test_dir, "global_runs.json")
|
|
31
33
|
self.approval_file = os.path.join(self.test_dir, "manual_approval.json")
|
|
32
34
|
|
|
33
|
-
if hasattr(self,
|
|
35
|
+
if hasattr(self, "_automation_manager"):
|
|
34
36
|
del self._automation_manager
|
|
35
37
|
|
|
36
38
|
# Initialize empty tracking files
|
|
37
|
-
with open(self.pr_attempts_file,
|
|
39
|
+
with open(self.pr_attempts_file, "w") as f:
|
|
38
40
|
json.dump({}, f)
|
|
39
|
-
with open(self.global_runs_file,
|
|
41
|
+
with open(self.global_runs_file, "w") as f:
|
|
40
42
|
json.dump({"total_runs": 0, "start_date": datetime.now().isoformat()}, f)
|
|
41
|
-
with open(self.approval_file,
|
|
43
|
+
with open(self.approval_file, "w") as f:
|
|
42
44
|
json.dump({"approved": False, "approval_date": None}, f)
|
|
43
45
|
|
|
44
46
|
def tearDown(self):
|
|
45
47
|
"""Clean up test files"""
|
|
46
48
|
shutil.rmtree(self.test_dir)
|
|
47
49
|
|
|
48
|
-
# Matrix 1: PR Attempt Limits (
|
|
50
|
+
# Matrix 1: PR Attempt Limits (50 attempts per PR - counts ALL attempts)
|
|
49
51
|
def test_pr_attempt_limit_1_should_allow(self):
|
|
50
52
|
"""RED: First attempt on PR #1001 should be allowed"""
|
|
51
53
|
# This test will FAIL initially - no implementation exists
|
|
@@ -53,40 +55,48 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
53
55
|
self.assertTrue(result)
|
|
54
56
|
self.assertEqual(self.automation_manager.get_pr_attempts(1001), 0)
|
|
55
57
|
|
|
56
|
-
def
|
|
57
|
-
"""RED:
|
|
58
|
-
# Set up
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
self.automation_manager.record_pr_attempt(1001, "failure")
|
|
58
|
+
def test_pr_attempt_limit_49_should_allow(self):
|
|
59
|
+
"""RED: 49th attempt on PR #1001 should be allowed"""
|
|
60
|
+
# Set up 48 previous attempts (mix of success and failure - all count)
|
|
61
|
+
for i in range(48):
|
|
62
|
+
result_type = "success" if i % 2 == 0 else "failure"
|
|
63
|
+
self.automation_manager.record_pr_attempt(1001, result_type)
|
|
63
64
|
|
|
64
65
|
result = self.automation_manager.can_process_pr(1001)
|
|
65
66
|
self.assertTrue(result)
|
|
66
|
-
self.assertEqual(self.automation_manager.get_pr_attempts(1001),
|
|
67
|
+
self.assertEqual(self.automation_manager.get_pr_attempts(1001), 48)
|
|
67
68
|
|
|
68
|
-
def
|
|
69
|
-
"""RED:
|
|
70
|
-
# Set up
|
|
71
|
-
for
|
|
72
|
-
|
|
69
|
+
def test_pr_attempt_limit_50_should_block(self):
|
|
70
|
+
"""RED: 50th attempt on PR #1001 should be blocked (at limit)"""
|
|
71
|
+
# Set up 50 previous attempts (max limit reached - mix of success and failure)
|
|
72
|
+
for i in range(50):
|
|
73
|
+
result_type = "success" if i % 2 == 0 else "failure"
|
|
74
|
+
self.automation_manager.record_pr_attempt(1001, result_type)
|
|
73
75
|
|
|
74
76
|
result = self.automation_manager.can_process_pr(1001)
|
|
75
77
|
self.assertFalse(result)
|
|
76
|
-
self.assertEqual(self.automation_manager.get_pr_attempts(1001),
|
|
78
|
+
self.assertEqual(self.automation_manager.get_pr_attempts(1001), 50)
|
|
77
79
|
|
|
78
|
-
def
|
|
79
|
-
"""
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.automation_manager.record_pr_attempt(1001, "failure")
|
|
84
|
-
self.automation_manager.record_pr_attempt(1001, "success")
|
|
80
|
+
def test_pr_attempt_all_attempts_count(self):
|
|
81
|
+
"""NEW BEHAVIOR: All attempts (success + failure) count toward limit"""
|
|
82
|
+
# Record 25 successes
|
|
83
|
+
for _ in range(25):
|
|
84
|
+
self.automation_manager.record_pr_attempt(1001, "success")
|
|
85
85
|
|
|
86
|
-
#
|
|
86
|
+
# Record 24 failures
|
|
87
|
+
for _ in range(24):
|
|
88
|
+
self.automation_manager.record_pr_attempt(1001, "failure")
|
|
89
|
+
|
|
90
|
+
# Total: 49 attempts - should still allow one more
|
|
87
91
|
result = self.automation_manager.can_process_pr(1001)
|
|
88
92
|
self.assertTrue(result)
|
|
89
|
-
self.assertEqual(self.automation_manager.get_pr_attempts(1001),
|
|
93
|
+
self.assertEqual(self.automation_manager.get_pr_attempts(1001), 49)
|
|
94
|
+
|
|
95
|
+
# Record one more (50th) - should now be at limit
|
|
96
|
+
self.automation_manager.record_pr_attempt(1001, "success")
|
|
97
|
+
result = self.automation_manager.can_process_pr(1001)
|
|
98
|
+
self.assertFalse(result)
|
|
99
|
+
self.assertEqual(self.automation_manager.get_pr_attempts(1001), 50)
|
|
90
100
|
|
|
91
101
|
# Matrix 2: Global Run Limits (50 total runs)
|
|
92
102
|
def test_global_run_limit_1_should_allow(self):
|
|
@@ -150,18 +160,19 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
150
160
|
|
|
151
161
|
# Matrix 4: Email Notification System
|
|
152
162
|
@patch.dict(os.environ, {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
"SMTP_SERVER": "smtp.example.com",
|
|
164
|
+
"SMTP_PORT": "587",
|
|
165
|
+
"EMAIL_USER": "test@example.com",
|
|
166
|
+
"EMAIL_PASS": "testpass",
|
|
167
|
+
"EMAIL_TO": "admin@example.com"
|
|
158
168
|
})
|
|
159
|
-
@patch(
|
|
169
|
+
@patch("smtplib.SMTP")
|
|
160
170
|
def test_email_sent_when_pr_limit_reached(self, mock_smtp):
|
|
161
|
-
"""RED: Email should be sent when PR reaches
|
|
162
|
-
# Set up
|
|
163
|
-
for
|
|
164
|
-
|
|
171
|
+
"""RED: Email should be sent when PR reaches 50 attempts"""
|
|
172
|
+
# Set up 50 attempts to trigger notification (mix of success and failure)
|
|
173
|
+
for i in range(50):
|
|
174
|
+
result_type = "success" if i % 2 == 0 else "failure"
|
|
175
|
+
self.automation_manager.record_pr_attempt(1001, result_type)
|
|
165
176
|
|
|
166
177
|
# Should trigger email
|
|
167
178
|
self.automation_manager.check_and_notify_limits()
|
|
@@ -170,13 +181,13 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
170
181
|
mock_smtp.assert_called_once()
|
|
171
182
|
|
|
172
183
|
@patch.dict(os.environ, {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
"SMTP_SERVER": "smtp.example.com",
|
|
185
|
+
"SMTP_PORT": "587",
|
|
186
|
+
"EMAIL_USER": "test@example.com",
|
|
187
|
+
"EMAIL_PASS": "testpass",
|
|
188
|
+
"EMAIL_TO": "admin@example.com"
|
|
178
189
|
})
|
|
179
|
-
@patch(
|
|
190
|
+
@patch("smtplib.SMTP")
|
|
180
191
|
def test_email_sent_when_global_limit_reached(self, mock_smtp):
|
|
181
192
|
"""RED: Email should be sent when global limit of 50 is reached"""
|
|
182
193
|
# Set up 50 runs to trigger notification
|
|
@@ -218,7 +229,6 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
218
229
|
def test_concurrent_pr_attempts_thread_safe(self):
|
|
219
230
|
"""RED: Concurrent PR attempts should be thread-safe"""
|
|
220
231
|
import threading
|
|
221
|
-
import time
|
|
222
232
|
|
|
223
233
|
# Create a single manager instance explicitly for this test
|
|
224
234
|
manager = AutomationSafetyManager(self.test_dir)
|
|
@@ -228,9 +238,9 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
228
238
|
result = manager.try_process_pr(1001)
|
|
229
239
|
results.append(result)
|
|
230
240
|
|
|
231
|
-
# Start
|
|
241
|
+
# Start 55 concurrent threads (more than limit of 50)
|
|
232
242
|
threads = []
|
|
233
|
-
for _ in range(
|
|
243
|
+
for _ in range(55):
|
|
234
244
|
t = threading.Thread(target=attempt_pr)
|
|
235
245
|
threads.append(t)
|
|
236
246
|
t.start()
|
|
@@ -239,29 +249,31 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
239
249
|
for t in threads:
|
|
240
250
|
t.join()
|
|
241
251
|
|
|
242
|
-
# Should have exactly
|
|
252
|
+
# Should have exactly 50 successful attempts (limit)
|
|
243
253
|
successful_attempts = sum(results)
|
|
244
|
-
self.assertEqual(successful_attempts,
|
|
254
|
+
self.assertEqual(successful_attempts, 50)
|
|
245
255
|
|
|
246
256
|
# Matrix 7: Configuration Management
|
|
247
|
-
def
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
257
|
+
def test_limits_configurable_via_constructor_overrides(self):
|
|
258
|
+
"""Safety limits should be configurable via explicit parameters (no env vars)."""
|
|
259
|
+
manager = AutomationSafetyManager(
|
|
260
|
+
self.test_dir,
|
|
261
|
+
limits={
|
|
262
|
+
"pr_limit": 3,
|
|
263
|
+
"global_limit": 25,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Should use custom limits
|
|
268
|
+
self.assertEqual(manager.pr_limit, 3)
|
|
269
|
+
self.assertEqual(manager.global_limit, 25)
|
|
258
270
|
|
|
259
271
|
def test_default_limits_when_no_config(self):
|
|
260
272
|
"""RED: Should use default limits when no configuration provided"""
|
|
261
273
|
manager = AutomationSafetyManager(self.test_dir)
|
|
262
274
|
|
|
263
|
-
# Should use defaults
|
|
264
|
-
self.assertEqual(manager.pr_limit,
|
|
275
|
+
# Should use defaults (pr_limit updated to 50)
|
|
276
|
+
self.assertEqual(manager.pr_limit, 50)
|
|
265
277
|
self.assertEqual(manager.global_limit, 50)
|
|
266
278
|
|
|
267
279
|
# Matrix 8: Daily Reset Functionality (50 runs per day)
|
|
@@ -307,23 +319,23 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
307
319
|
"total_runs": 50,
|
|
308
320
|
"start_date": datetime(2025, 9, 30, 12, 0, 0).isoformat()
|
|
309
321
|
}
|
|
310
|
-
with open(self.global_runs_file,
|
|
322
|
+
with open(self.global_runs_file, "w") as f:
|
|
311
323
|
json.dump(legacy_data, f)
|
|
312
324
|
|
|
313
|
-
if hasattr(self,
|
|
325
|
+
if hasattr(self, "_automation_manager"):
|
|
314
326
|
del self._automation_manager
|
|
315
327
|
|
|
316
328
|
# First run after upgrade should reset the stale counter
|
|
317
329
|
self.assertTrue(self.automation_manager.can_start_global_run())
|
|
318
330
|
self.assertEqual(self.automation_manager.get_global_runs(), 0)
|
|
319
331
|
|
|
320
|
-
@patch(
|
|
332
|
+
@patch("jleechanorg_pr_automation.automation_safety_manager.datetime")
|
|
321
333
|
def test_daily_reset_new_day_resets_counter(self, mock_datetime):
|
|
322
334
|
"""RED: Counter should reset to 0 when a new day starts"""
|
|
323
335
|
# Day 1: Record 50 runs
|
|
324
336
|
day1 = datetime(2025, 10, 1, 10, 0, 0)
|
|
325
337
|
mock_datetime.now.return_value = day1
|
|
326
|
-
mock_datetime.side_effect =
|
|
338
|
+
mock_datetime.side_effect = datetime
|
|
327
339
|
|
|
328
340
|
for _ in range(50):
|
|
329
341
|
self.automation_manager.record_global_run()
|
|
@@ -341,10 +353,10 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
341
353
|
self.assertTrue(result)
|
|
342
354
|
self.assertEqual(self.automation_manager.get_global_runs(), 0)
|
|
343
355
|
|
|
344
|
-
@patch(
|
|
356
|
+
@patch("jleechanorg_pr_automation.automation_safety_manager.datetime")
|
|
345
357
|
def test_daily_reset_multiple_days(self, mock_datetime):
|
|
346
358
|
"""RED: Counter should reset each day for multiple days"""
|
|
347
|
-
mock_datetime.side_effect =
|
|
359
|
+
mock_datetime.side_effect = datetime
|
|
348
360
|
|
|
349
361
|
# Day 1: 50 runs
|
|
350
362
|
day1 = datetime(2025, 10, 1, 10, 0, 0)
|
|
@@ -366,10 +378,10 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
366
378
|
mock_datetime.now.return_value = day3
|
|
367
379
|
self.assertEqual(self.automation_manager.get_global_runs(), 0)
|
|
368
380
|
|
|
369
|
-
@patch(
|
|
381
|
+
@patch("jleechanorg_pr_automation.automation_safety_manager.datetime")
|
|
370
382
|
def test_daily_reset_midnight_transition(self, mock_datetime):
|
|
371
383
|
"""RED: Counter should reset at midnight transition"""
|
|
372
|
-
mock_datetime.side_effect =
|
|
384
|
+
mock_datetime.side_effect = datetime
|
|
373
385
|
|
|
374
386
|
# 23:59:59 on Day 1 - at limit
|
|
375
387
|
before_midnight = datetime(2025, 10, 1, 23, 59, 59)
|
|
@@ -390,7 +402,7 @@ class TestAutomationSafetyLimits(unittest.TestCase):
|
|
|
390
402
|
def automation_manager(self):
|
|
391
403
|
"""RED: This property will fail - no AutomationSafetyManager exists yet"""
|
|
392
404
|
# This will fail until we implement the class in GREEN phase
|
|
393
|
-
if not hasattr(self,
|
|
405
|
+
if not hasattr(self, "_automation_manager"):
|
|
394
406
|
self._automation_manager = AutomationSafetyManager(self.test_dir)
|
|
395
407
|
return self._automation_manager
|
|
396
408
|
|
|
@@ -425,7 +437,7 @@ class TestAutomationIntegration(unittest.TestCase):
|
|
|
425
437
|
def test_shell_script_respects_safety_limits(self):
|
|
426
438
|
"""RED: Shell script should check safety limits before processing"""
|
|
427
439
|
# This test will fail - existing script doesn't have safety checks
|
|
428
|
-
with patch(
|
|
440
|
+
with patch("subprocess.run") as mock_run:
|
|
429
441
|
mock_run.return_value.returncode = 1 # Safety limit hit
|
|
430
442
|
|
|
431
443
|
result = self.run_automation_script()
|
|
@@ -446,7 +458,7 @@ class TestAutomationIntegration(unittest.TestCase):
|
|
|
446
458
|
import subprocess
|
|
447
459
|
return subprocess.run([
|
|
448
460
|
"/Users/jleechan/projects/worktree_worker2/automation/simple_pr_batch.sh"
|
|
449
|
-
], capture_output=True, text=True)
|
|
461
|
+
], check=False, capture_output=True, text=True)
|
|
450
462
|
|
|
451
463
|
def read_launchd_plist(self):
|
|
452
464
|
"""Helper to read launchd plist file"""
|
|
@@ -455,7 +467,7 @@ class TestAutomationIntegration(unittest.TestCase):
|
|
|
455
467
|
return f.read()
|
|
456
468
|
|
|
457
469
|
|
|
458
|
-
if __name__ ==
|
|
470
|
+
if __name__ == "__main__":
|
|
459
471
|
# RED Phase: Run tests to confirm they FAIL
|
|
460
472
|
print("🔴 RED Phase: Running failing tests for automation safety limits")
|
|
461
473
|
print("Expected: ALL TESTS SHOULD FAIL (no implementation exists)")
|
|
@@ -4,15 +4,14 @@ Comprehensive test suite for AutomationSafetyManager
|
|
|
4
4
|
Using TDD methodology with 150+ test cases covering all safety logic
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import pytest
|
|
8
7
|
import json
|
|
9
|
-
import tempfile
|
|
10
8
|
import os
|
|
9
|
+
import tempfile
|
|
11
10
|
import threading
|
|
12
|
-
import time
|
|
13
|
-
from pathlib import Path
|
|
14
11
|
from datetime import datetime, timedelta
|
|
15
|
-
from unittest.mock import Mock, patch
|
|
12
|
+
from unittest.mock import Mock, patch
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
16
15
|
|
|
17
16
|
# Import the automation safety manager using proper Python module path
|
|
18
17
|
from jleechanorg_pr_automation.automation_safety_manager import AutomationSafetyManager
|
|
@@ -47,7 +46,7 @@ class TestAutomationSafetyManagerInit:
|
|
|
47
46
|
"pr_limit": 3,
|
|
48
47
|
"daily_limit": 100
|
|
49
48
|
}
|
|
50
|
-
with open(config_file,
|
|
49
|
+
with open(config_file, "w") as f:
|
|
51
50
|
json.dump(config_data, f)
|
|
52
51
|
|
|
53
52
|
manager = AutomationSafetyManager(temp_dir)
|
|
@@ -62,7 +61,7 @@ class TestAutomationSafetyManagerInit:
|
|
|
62
61
|
config_file = os.path.join(temp_dir, "automation_safety_config.json")
|
|
63
62
|
assert os.path.exists(config_file)
|
|
64
63
|
|
|
65
|
-
with open(config_file
|
|
64
|
+
with open(config_file) as f:
|
|
66
65
|
config = json.load(f)
|
|
67
66
|
assert "global_limit" in config
|
|
68
67
|
assert "pr_limit" in config
|
|
@@ -72,7 +71,7 @@ class TestAutomationSafetyManagerInit:
|
|
|
72
71
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
73
72
|
# Create invalid config file
|
|
74
73
|
config_file = os.path.join(temp_dir, "automation_safety_config.json")
|
|
75
|
-
with open(config_file,
|
|
74
|
+
with open(config_file, "w") as f:
|
|
76
75
|
f.write("{ invalid json")
|
|
77
76
|
|
|
78
77
|
manager = AutomationSafetyManager(temp_dir)
|
|
@@ -224,12 +223,12 @@ class TestPRLimits:
|
|
|
224
223
|
assert manager.can_process_pr(pr_key) == True
|
|
225
224
|
|
|
226
225
|
def test_can_process_pr_at_limit(self, manager):
|
|
227
|
-
"""Test PR processing denied when at limit"""
|
|
226
|
+
"""Test PR processing denied when at failure limit"""
|
|
228
227
|
pr_key = "test-repo-123"
|
|
229
228
|
|
|
230
|
-
#
|
|
229
|
+
# Record failures up to limit
|
|
231
230
|
for _ in range(manager.pr_limit):
|
|
232
|
-
manager.record_pr_attempt(pr_key, "
|
|
231
|
+
manager.record_pr_attempt(pr_key, "failure")
|
|
233
232
|
|
|
234
233
|
assert manager.can_process_pr(pr_key) == False
|
|
235
234
|
|
|
@@ -337,11 +336,11 @@ class TestEmailNotifications:
|
|
|
337
336
|
yield AutomationSafetyManager(temp_dir)
|
|
338
337
|
|
|
339
338
|
@patch.dict(os.environ, {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
339
|
+
"SMTP_SERVER": "smtp.example.com",
|
|
340
|
+
"SMTP_PORT": "587",
|
|
341
|
+
"EMAIL_USER": "test@example.com",
|
|
342
|
+
"EMAIL_PASS": "password",
|
|
343
|
+
"EMAIL_TO": "admin@example.com"
|
|
345
344
|
})
|
|
346
345
|
def test_email_config_complete(self, manager):
|
|
347
346
|
"""Test email configuration detection when complete"""
|
|
@@ -353,8 +352,8 @@ class TestEmailNotifications:
|
|
|
353
352
|
assert manager._is_email_configured() == False
|
|
354
353
|
|
|
355
354
|
@patch.dict(os.environ, {
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
"SMTP_SERVER": "smtp.example.com",
|
|
356
|
+
"EMAIL_USER": "test@example.com"
|
|
358
357
|
# Missing SMTP_PORT, EMAIL_PASS, EMAIL_TO
|
|
359
358
|
})
|
|
360
359
|
def test_email_config_partial(self, manager):
|
|
@@ -362,13 +361,13 @@ class TestEmailNotifications:
|
|
|
362
361
|
assert manager._is_email_configured() == False
|
|
363
362
|
|
|
364
363
|
@patch.dict(os.environ, {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
364
|
+
"SMTP_SERVER": "smtp.example.com",
|
|
365
|
+
"SMTP_PORT": "587",
|
|
366
|
+
"EMAIL_USER": "test@example.com",
|
|
367
|
+
"EMAIL_PASS": "password",
|
|
368
|
+
"EMAIL_TO": "admin@example.com"
|
|
370
369
|
})
|
|
371
|
-
@patch(
|
|
370
|
+
@patch("smtplib.SMTP")
|
|
372
371
|
def test_send_notification_success(self, mock_smtp, manager):
|
|
373
372
|
"""Test successful email notification sending"""
|
|
374
373
|
mock_server = Mock()
|
|
@@ -377,54 +376,54 @@ class TestEmailNotifications:
|
|
|
377
376
|
result = manager.send_notification("Test Subject", "Test message")
|
|
378
377
|
|
|
379
378
|
assert result == True
|
|
380
|
-
mock_smtp.assert_called_once_with(
|
|
379
|
+
mock_smtp.assert_called_once_with("smtp.example.com", 587)
|
|
381
380
|
mock_server.starttls.assert_called_once()
|
|
382
|
-
mock_server.login.assert_called_once_with(
|
|
381
|
+
mock_server.login.assert_called_once_with("test@example.com", "password")
|
|
383
382
|
mock_server.send_message.assert_called_once()
|
|
384
383
|
mock_server.quit.assert_called_once()
|
|
385
384
|
|
|
386
385
|
@patch.dict(os.environ, {}, clear=True)
|
|
387
386
|
def test_send_notification_no_config(self, manager):
|
|
388
387
|
"""Test email notification when not configured"""
|
|
389
|
-
with patch.object(manager.logger,
|
|
388
|
+
with patch.object(manager.logger, "info") as mock_info:
|
|
390
389
|
result = manager.send_notification("Test", "Message")
|
|
391
390
|
|
|
392
391
|
assert result == False
|
|
393
392
|
mock_info.assert_called_with("Email configuration incomplete - skipping notification")
|
|
394
393
|
|
|
395
394
|
@patch.dict(os.environ, {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
395
|
+
"SMTP_SERVER": "smtp.example.com",
|
|
396
|
+
"SMTP_PORT": "587",
|
|
397
|
+
"EMAIL_USER": "test@example.com",
|
|
398
|
+
"EMAIL_PASS": "password",
|
|
399
|
+
"EMAIL_TO": "admin@example.com"
|
|
401
400
|
})
|
|
402
|
-
@patch(
|
|
401
|
+
@patch("smtplib.SMTP")
|
|
403
402
|
def test_send_notification_smtp_error(self, mock_smtp, manager):
|
|
404
403
|
"""Test email notification with SMTP error"""
|
|
405
404
|
mock_smtp.side_effect = Exception("SMTP connection failed")
|
|
406
405
|
|
|
407
|
-
with patch.object(manager.logger,
|
|
406
|
+
with patch.object(manager.logger, "error") as mock_error:
|
|
408
407
|
result = manager.send_notification("Test", "Message")
|
|
409
408
|
|
|
410
409
|
assert result == False
|
|
411
410
|
mock_error.assert_called()
|
|
412
411
|
|
|
413
412
|
@patch.dict(os.environ, {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
"SMTP_SERVER": "smtp.example.com",
|
|
414
|
+
"SMTP_PORT": "587",
|
|
415
|
+
"EMAIL_USER": "test@example.com",
|
|
416
|
+
"EMAIL_PASS": "password",
|
|
417
|
+
"EMAIL_TO": "admin@example.com"
|
|
419
418
|
})
|
|
420
|
-
@patch(
|
|
419
|
+
@patch("smtplib.SMTP")
|
|
421
420
|
def test_send_notification_login_error(self, mock_smtp, manager):
|
|
422
421
|
"""Test email notification with login error"""
|
|
423
422
|
mock_server = Mock()
|
|
424
423
|
mock_server.login.side_effect = Exception("Authentication failed")
|
|
425
424
|
mock_smtp.return_value = mock_server
|
|
426
425
|
|
|
427
|
-
with patch.object(manager.logger,
|
|
426
|
+
with patch.object(manager.logger, "error") as mock_error:
|
|
428
427
|
result = manager.send_notification("Test", "Message")
|
|
429
428
|
|
|
430
429
|
assert result == False
|
|
@@ -493,7 +492,7 @@ class TestFileLocking:
|
|
|
493
492
|
global_runs_file = os.path.join(manager.data_dir, "global_runs.json")
|
|
494
493
|
manager.record_global_run() # Create the file first
|
|
495
494
|
|
|
496
|
-
with open(global_runs_file,
|
|
495
|
+
with open(global_runs_file, "w") as f:
|
|
497
496
|
f.write("{ corrupted json")
|
|
498
497
|
|
|
499
498
|
# Should recover gracefully
|
|
@@ -502,8 +501,8 @@ class TestFileLocking:
|
|
|
502
501
|
|
|
503
502
|
def test_permission_error_handling(self, manager):
|
|
504
503
|
"""Test handling of file permission errors"""
|
|
505
|
-
with patch(
|
|
506
|
-
with patch.object(manager.logger,
|
|
504
|
+
with patch("jleechanorg_pr_automation.utils.json_manager.write_json", return_value=False):
|
|
505
|
+
with patch.object(manager.logger, "error") as mock_error:
|
|
507
506
|
# Should not raise exception
|
|
508
507
|
manager.record_global_run()
|
|
509
508
|
mock_error.assert_called()
|
|
@@ -523,7 +522,7 @@ class TestConfigurationManagement:
|
|
|
523
522
|
"email_notifications": True,
|
|
524
523
|
"max_pr_size": 1000
|
|
525
524
|
}
|
|
526
|
-
with open(config_file,
|
|
525
|
+
with open(config_file, "w") as f:
|
|
527
526
|
json.dump(config_data, f)
|
|
528
527
|
|
|
529
528
|
manager = AutomationSafetyManager(temp_dir)
|
|
@@ -538,7 +537,7 @@ class TestConfigurationManagement:
|
|
|
538
537
|
"global_limit": 15
|
|
539
538
|
# Missing other settings
|
|
540
539
|
}
|
|
541
|
-
with open(config_file,
|
|
540
|
+
with open(config_file, "w") as f:
|
|
542
541
|
json.dump(config_data, f)
|
|
543
542
|
|
|
544
543
|
manager = AutomationSafetyManager(temp_dir)
|
|
@@ -553,7 +552,7 @@ class TestConfigurationManagement:
|
|
|
553
552
|
config_file = os.path.join(temp_dir, "automation_safety_config.json")
|
|
554
553
|
assert os.path.exists(config_file)
|
|
555
554
|
|
|
556
|
-
with open(config_file
|
|
555
|
+
with open(config_file) as f:
|
|
557
556
|
config = json.load(f)
|
|
558
557
|
assert "global_limit" in config
|
|
559
558
|
assert "pr_limit" in config
|
|
@@ -599,13 +598,13 @@ class TestIntegrationScenarios:
|
|
|
599
598
|
assert attempts[0]["result"] == "success"
|
|
600
599
|
|
|
601
600
|
def test_hitting_pr_limits(self, manager):
|
|
602
|
-
"""Test behavior when hitting PR limits"""
|
|
601
|
+
"""Test behavior when hitting PR failure limits"""
|
|
603
602
|
pr_key = "test-repo-limit"
|
|
604
603
|
|
|
605
|
-
#
|
|
604
|
+
# Fail up to limit
|
|
606
605
|
for i in range(manager.pr_limit):
|
|
607
606
|
assert manager.can_process_pr(pr_key) == True
|
|
608
|
-
manager.record_pr_attempt(pr_key, "
|
|
607
|
+
manager.record_pr_attempt(pr_key, "failure")
|
|
609
608
|
|
|
610
609
|
# Should now be at limit
|
|
611
610
|
assert manager.can_process_pr(pr_key) == False
|
|
@@ -631,7 +630,8 @@ class TestIntegrationScenarios:
|
|
|
631
630
|
manager.record_pr_attempt(pr_key, result)
|
|
632
631
|
|
|
633
632
|
attempts = manager.get_pr_attempt_list(pr_key)
|
|
634
|
-
|
|
633
|
+
# Attempt history should remain bounded: failures + most recent non-failure
|
|
634
|
+
assert len(attempts) <= manager.pr_limit + 1
|
|
635
635
|
|
|
636
636
|
# Verify results are recorded correctly
|
|
637
637
|
recorded_results = [attempt["result"] for attempt in attempts]
|
|
@@ -655,5 +655,5 @@ class TestIntegrationScenarios:
|
|
|
655
655
|
assert len(attempts1) == len(attempts2)
|
|
656
656
|
|
|
657
657
|
|
|
658
|
-
if __name__ ==
|
|
659
|
-
pytest.main([__file__,
|
|
658
|
+
if __name__ == "__main__":
|
|
659
|
+
pytest.main([__file__, "-v", "--tb=short"])
|
|
@@ -10,7 +10,7 @@ class TestCodexActorMatching(unittest.TestCase):
|
|
|
10
10
|
"""Validate detection of Codex-authored commits."""
|
|
11
11
|
|
|
12
12
|
def setUp(self) -> None:
|
|
13
|
-
self.monitor = JleechanorgPRMonitor()
|
|
13
|
+
self.monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
14
14
|
|
|
15
15
|
def test_detects_codex_via_actor_fields(self) -> None:
|
|
16
16
|
commit_details = {
|