jleechanorg-pr-automation 0.1.1__py3-none-any.whl → 0.2.45__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. jleechanorg_pr_automation/STORAGE_STATE_TESTING_PROTOCOL.md +326 -0
  2. jleechanorg_pr_automation/__init__.py +64 -9
  3. jleechanorg_pr_automation/automation_safety_manager.py +306 -95
  4. jleechanorg_pr_automation/automation_safety_wrapper.py +13 -19
  5. jleechanorg_pr_automation/automation_utils.py +87 -65
  6. jleechanorg_pr_automation/check_codex_comment.py +7 -1
  7. jleechanorg_pr_automation/codex_branch_updater.py +21 -9
  8. jleechanorg_pr_automation/codex_config.py +70 -3
  9. jleechanorg_pr_automation/jleechanorg_pr_monitor.py +1954 -234
  10. jleechanorg_pr_automation/logging_utils.py +86 -0
  11. jleechanorg_pr_automation/openai_automation/__init__.py +3 -0
  12. jleechanorg_pr_automation/openai_automation/codex_github_mentions.py +1111 -0
  13. jleechanorg_pr_automation/openai_automation/debug_page_content.py +88 -0
  14. jleechanorg_pr_automation/openai_automation/oracle_cli.py +364 -0
  15. jleechanorg_pr_automation/openai_automation/test_auth_restoration.py +244 -0
  16. jleechanorg_pr_automation/openai_automation/test_codex_comprehensive.py +355 -0
  17. jleechanorg_pr_automation/openai_automation/test_codex_integration.py +254 -0
  18. jleechanorg_pr_automation/orchestrated_pr_runner.py +516 -0
  19. jleechanorg_pr_automation/tests/__init__.py +0 -0
  20. jleechanorg_pr_automation/tests/test_actionable_counting_matrix.py +84 -86
  21. jleechanorg_pr_automation/tests/test_attempt_limit_logic.py +124 -0
  22. jleechanorg_pr_automation/tests/test_automation_marker_functions.py +175 -0
  23. jleechanorg_pr_automation/tests/test_automation_over_running_reproduction.py +9 -11
  24. jleechanorg_pr_automation/tests/test_automation_safety_limits.py +91 -79
  25. jleechanorg_pr_automation/tests/test_automation_safety_manager_comprehensive.py +53 -53
  26. jleechanorg_pr_automation/tests/test_codex_actor_matching.py +1 -1
  27. jleechanorg_pr_automation/tests/test_fixpr_prompt.py +54 -0
  28. jleechanorg_pr_automation/tests/test_fixpr_return_value.py +140 -0
  29. jleechanorg_pr_automation/tests/test_graphql_error_handling.py +26 -26
  30. jleechanorg_pr_automation/tests/test_model_parameter.py +317 -0
  31. jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +697 -0
  32. jleechanorg_pr_automation/tests/test_packaging_integration.py +127 -0
  33. jleechanorg_pr_automation/tests/test_pr_filtering_matrix.py +246 -193
  34. jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +354 -0
  35. jleechanorg_pr_automation/tests/test_pr_targeting.py +102 -7
  36. jleechanorg_pr_automation/tests/test_version_consistency.py +51 -0
  37. jleechanorg_pr_automation/tests/test_workflow_specific_limits.py +202 -0
  38. jleechanorg_pr_automation/tests/test_workspace_dispatch_missing_dir.py +119 -0
  39. jleechanorg_pr_automation/utils.py +81 -56
  40. jleechanorg_pr_automation-0.2.45.dist-info/METADATA +864 -0
  41. jleechanorg_pr_automation-0.2.45.dist-info/RECORD +45 -0
  42. jleechanorg_pr_automation-0.1.1.dist-info/METADATA +0 -222
  43. jleechanorg_pr_automation-0.1.1.dist-info/RECORD +0 -23
  44. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/WHEEL +0 -0
  45. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/entry_points.txt +0 -0
  46. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/top_level.txt +0 -0
@@ -8,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 json
17
- from datetime import datetime, timedelta
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('--manual_override', type=str, help='Correct command')
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(['--manual_override', 'test@example.com'])
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, 'test@example.com')
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, 'check_rate_limit'),
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__ == '__main__':
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 5 per PR)
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, MagicMock
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, '_automation_manager'):
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, 'w') as f:
39
+ with open(self.pr_attempts_file, "w") as f:
38
40
  json.dump({}, f)
39
- with open(self.global_runs_file, 'w') as f:
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, 'w') as f:
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 (5 attempts per PR)
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 test_pr_attempt_limit_5_should_allow(self):
57
- """RED: 5th attempt on PR #1001 should be allowed"""
58
- # Set up 4 previous attempts
59
- self.automation_manager.record_pr_attempt(1001, "failure")
60
- self.automation_manager.record_pr_attempt(1001, "failure")
61
- self.automation_manager.record_pr_attempt(1001, "failure")
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), 4)
67
+ self.assertEqual(self.automation_manager.get_pr_attempts(1001), 48)
67
68
 
68
- def test_pr_attempt_limit_6_should_block(self):
69
- """RED: 6th attempt on PR #1001 should be blocked"""
70
- # Set up 5 previous attempts (max limit reached)
71
- for _ in range(5):
72
- self.automation_manager.record_pr_attempt(1001, "failure")
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), 5)
78
+ self.assertEqual(self.automation_manager.get_pr_attempts(1001), 50)
77
79
 
78
- def test_pr_attempt_success_resets_counter(self):
79
- """RED: Successful PR attempt should reset counter"""
80
- # Set up 3 failures then 1 success
81
- self.automation_manager.record_pr_attempt(1001, "failure")
82
- self.automation_manager.record_pr_attempt(1001, "failure")
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
- # Counter should reset, allowing new attempts
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), 0)
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
- 'SMTP_SERVER': 'smtp.example.com',
154
- 'SMTP_PORT': '587',
155
- 'EMAIL_USER': 'test@example.com',
156
- 'EMAIL_PASS': 'testpass',
157
- 'EMAIL_TO': 'admin@example.com'
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('smtplib.SMTP')
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 5 attempts"""
162
- # Set up 5 attempts to trigger notification
163
- for _ in range(5):
164
- self.automation_manager.record_pr_attempt(1001, "failure")
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
- 'SMTP_SERVER': 'smtp.example.com',
174
- 'SMTP_PORT': '587',
175
- 'EMAIL_USER': 'test@example.com',
176
- 'EMAIL_PASS': 'testpass',
177
- 'EMAIL_TO': 'admin@example.com'
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('smtplib.SMTP')
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 10 concurrent threads
241
+ # Start 55 concurrent threads (more than limit of 50)
232
242
  threads = []
233
- for _ in range(10):
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 5 successful attempts (limit)
252
+ # Should have exactly 50 successful attempts (limit)
243
253
  successful_attempts = sum(results)
244
- self.assertEqual(successful_attempts, 5)
254
+ self.assertEqual(successful_attempts, 50)
245
255
 
246
256
  # Matrix 7: Configuration Management
247
- def test_limits_configurable_via_environment(self):
248
- """RED: Safety limits should be configurable via environment variables"""
249
- with patch.dict(os.environ, {
250
- 'AUTOMATION_PR_LIMIT': '3',
251
- 'AUTOMATION_GLOBAL_LIMIT': '25'
252
- }):
253
- manager = AutomationSafetyManager(self.test_dir)
254
-
255
- # Should use custom limits
256
- self.assertEqual(manager.pr_limit, 3)
257
- self.assertEqual(manager.global_limit, 25)
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, 5)
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, 'w') as f:
322
+ with open(self.global_runs_file, "w") as f:
311
323
  json.dump(legacy_data, f)
312
324
 
313
- if hasattr(self, '_automation_manager'):
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('jleechanorg_pr_automation.automation_safety_manager.datetime')
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 = lambda *args, **kw: datetime(*args, **kw)
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('jleechanorg_pr_automation.automation_safety_manager.datetime')
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 = lambda *args, **kw: datetime(*args, **kw)
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('jleechanorg_pr_automation.automation_safety_manager.datetime')
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 = lambda *args, **kw: datetime(*args, **kw)
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, '_automation_manager'):
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('subprocess.run') as mock_run:
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__ == '__main__':
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, MagicMock
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, 'w') as f:
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, 'r') as f:
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, 'w') as f:
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
- # Process PR up to limit
229
+ # Record failures up to limit
231
230
  for _ in range(manager.pr_limit):
232
- manager.record_pr_attempt(pr_key, "success")
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
- 'SMTP_SERVER': 'smtp.example.com',
341
- 'SMTP_PORT': '587',
342
- 'EMAIL_USER': 'test@example.com',
343
- 'EMAIL_PASS': 'password',
344
- 'EMAIL_TO': 'admin@example.com'
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
- 'SMTP_SERVER': 'smtp.example.com',
357
- 'EMAIL_USER': 'test@example.com'
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
- 'SMTP_SERVER': 'smtp.example.com',
366
- 'SMTP_PORT': '587',
367
- 'EMAIL_USER': 'test@example.com',
368
- 'EMAIL_PASS': 'password',
369
- 'EMAIL_TO': 'admin@example.com'
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('smtplib.SMTP')
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('smtp.example.com', 587)
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('test@example.com', 'password')
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, 'info') as mock_info:
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
- 'SMTP_SERVER': 'smtp.example.com',
397
- 'SMTP_PORT': '587',
398
- 'EMAIL_USER': 'test@example.com',
399
- 'EMAIL_PASS': 'password',
400
- 'EMAIL_TO': 'admin@example.com'
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('smtplib.SMTP')
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, 'error') as mock_error:
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
- 'SMTP_SERVER': 'smtp.example.com',
415
- 'SMTP_PORT': '587',
416
- 'EMAIL_USER': 'test@example.com',
417
- 'EMAIL_PASS': 'password',
418
- 'EMAIL_TO': 'admin@example.com'
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('smtplib.SMTP')
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, 'error') as mock_error:
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, 'w') as f:
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('jleechanorg_pr_automation.utils.json_manager.write_json', return_value=False):
506
- with patch.object(manager.logger, 'error') as mock_error:
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, 'w') as f:
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, 'w') as f:
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, 'r') as f:
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
- # Process up to limit
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, "success")
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
- assert len(attempts) <= manager.pr_limit
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__ == '__main__':
659
- pytest.main([__file__, '-v', '--tb=short'])
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 = {