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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test-Driven Development for Workflow-Specific Safety Limits
|
|
4
|
+
|
|
5
|
+
RED Phase: Tests should FAIL initially (implementation broken)
|
|
6
|
+
GREEN Phase: Fix implementation to make tests pass
|
|
7
|
+
|
|
8
|
+
Tests workflow-specific comment counting and safety limits:
|
|
9
|
+
- pr_automation workflow
|
|
10
|
+
- fix_comment workflow
|
|
11
|
+
- codex_update workflow
|
|
12
|
+
- fixpr workflow
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import unittest
|
|
16
|
+
from unittest.mock import Mock, patch
|
|
17
|
+
|
|
18
|
+
from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestWorkflowSpecificLimits(unittest.TestCase):
|
|
22
|
+
"""Test workflow-specific comment counting and safety limits"""
|
|
23
|
+
|
|
24
|
+
def setUp(self):
|
|
25
|
+
"""Set up test environment"""
|
|
26
|
+
with patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.AutomationSafetyManager'):
|
|
27
|
+
# Initialize with explicit test username to avoid environment dependency
|
|
28
|
+
self.monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
29
|
+
# Mock safety manager with workflow-specific limits
|
|
30
|
+
self.monitor.safety_manager.pr_automation_limit = 10
|
|
31
|
+
self.monitor.safety_manager.fix_comment_limit = 5
|
|
32
|
+
self.monitor.safety_manager.codex_update_limit = 10
|
|
33
|
+
self.monitor.safety_manager.fixpr_limit = 10
|
|
34
|
+
|
|
35
|
+
def test_count_pr_automation_comments(self):
|
|
36
|
+
"""Test counting PR automation comments (codex marker, not fix-comment)"""
|
|
37
|
+
comments = [
|
|
38
|
+
{"body": "<!-- codex-automation-commit:abc123 -->"},
|
|
39
|
+
{"body": "<!-- codex-automation-commit:def456 -->"},
|
|
40
|
+
{"body": "<!-- fix-comment-automation-commit:xyz789 -->"}, # Should NOT count
|
|
41
|
+
{"body": "Regular comment"},
|
|
42
|
+
]
|
|
43
|
+
count = self.monitor._count_workflow_comments(comments, "pr_automation")
|
|
44
|
+
self.assertEqual(count, 2, "Should count only codex-automation-commit comments without fix-comment marker")
|
|
45
|
+
|
|
46
|
+
def test_count_fix_comment_comments(self):
|
|
47
|
+
"""Test counting fix-comment workflow comments"""
|
|
48
|
+
comments = [
|
|
49
|
+
{"body": "<!-- fix-comment-automation-commit:abc123 -->", "author": {"login": "test-automation-user"}},
|
|
50
|
+
{"body": "<!-- fix-comment-automation-commit:def456 -->", "author": {"login": "test-automation-user"}},
|
|
51
|
+
{"body": "<!-- codex-automation-commit:xyz789 -->", "author": {"login": "test-automation-user"}}, # Should NOT count
|
|
52
|
+
{"body": "Regular comment", "author": {"login": "test-automation-user"}},
|
|
53
|
+
]
|
|
54
|
+
count = self.monitor._count_workflow_comments(comments, "fix_comment")
|
|
55
|
+
self.assertEqual(count, 2, "Should count only fix-comment-automation-commit comments")
|
|
56
|
+
|
|
57
|
+
def test_count_fix_comment_run_comments(self):
|
|
58
|
+
"""Test counting fix-comment queued run markers"""
|
|
59
|
+
comments = [
|
|
60
|
+
{"body": "<!-- fix-comment-run-automation-commit:gemini:abc123 -->", "author": {"login": "test-automation-user"}},
|
|
61
|
+
{"body": "<!-- fix-comment-run-automation-commit:codex:def456 -->", "author": {"login": "test-automation-user"}},
|
|
62
|
+
{"body": "<!-- codex-automation-commit:xyz789 -->", "author": {"login": "test-automation-user"}}, # Should NOT count
|
|
63
|
+
{"body": "Regular comment", "author": {"login": "test-automation-user"}},
|
|
64
|
+
]
|
|
65
|
+
count = self.monitor._count_workflow_comments(comments, "fix_comment")
|
|
66
|
+
self.assertEqual(count, 2, "Should count fix-comment-run-automation-commit comments for fix_comment workflow")
|
|
67
|
+
|
|
68
|
+
def test_count_codex_update_comments(self):
|
|
69
|
+
"""Test counting codex-update workflow comments (should always be 0)"""
|
|
70
|
+
comments = [
|
|
71
|
+
{"body": "<!-- codex-automation-commit:abc123 -->"},
|
|
72
|
+
{"body": "<!-- fix-comment-automation-commit:def456 -->"},
|
|
73
|
+
]
|
|
74
|
+
count = self.monitor._count_workflow_comments(comments, "codex_update")
|
|
75
|
+
self.assertEqual(count, 0, "Codex update workflow doesn't post PR comments, should always return 0")
|
|
76
|
+
|
|
77
|
+
def test_count_fixpr_comments(self):
|
|
78
|
+
"""Test counting fixpr workflow comments"""
|
|
79
|
+
comments = [
|
|
80
|
+
{"body": "<!-- fixpr-run-automation-commit:gemini:abc123 -->", "author": {"login": "test-automation-user"}},
|
|
81
|
+
{"body": "<!-- fixpr-run-automation-commit:codex:def456 -->", "author": {"login": "test-automation-user"}},
|
|
82
|
+
{"body": "<!-- codex-automation-commit:xyz789 -->", "author": {"login": "test-automation-user"}}, # Should NOT count
|
|
83
|
+
{"body": "<!-- fix-comment-automation-commit:ghi012 -->", "author": {"login": "test-automation-user"}}, # Should NOT count
|
|
84
|
+
{"body": "Regular comment", "author": {"login": "test-automation-user"}},
|
|
85
|
+
]
|
|
86
|
+
count = self.monitor._count_workflow_comments(comments, "fixpr")
|
|
87
|
+
self.assertEqual(count, 2, "Should count only fixpr-run-automation-commit comments for fixpr workflow")
|
|
88
|
+
|
|
89
|
+
def test_workflow_specific_limit_pr_automation(self):
|
|
90
|
+
"""Test that PR automation workflow uses its own limit"""
|
|
91
|
+
comments = [
|
|
92
|
+
{"body": f"<!-- codex-automation-commit:abc{i} -->"} for i in range(10)
|
|
93
|
+
]
|
|
94
|
+
count = self.monitor._count_workflow_comments(comments, "pr_automation")
|
|
95
|
+
# Should be at limit (10), not blocked yet
|
|
96
|
+
self.assertEqual(count, 10)
|
|
97
|
+
# 11th comment should exceed limit
|
|
98
|
+
comments.append({"body": "<!-- codex-automation-commit:abc11 -->"})
|
|
99
|
+
count = self.monitor._count_workflow_comments(comments, "pr_automation")
|
|
100
|
+
self.assertEqual(count, 11)
|
|
101
|
+
self.assertGreater(count, self.monitor.safety_manager.pr_automation_limit)
|
|
102
|
+
|
|
103
|
+
def test_workflow_specific_limit_fix_comment(self):
|
|
104
|
+
"""Test that fix-comment workflow uses its own limit (5)"""
|
|
105
|
+
comments = [
|
|
106
|
+
{"body": f"<!-- fix-comment-automation-commit:abc{i} -->", "author": {"login": "test-automation-user"}} for i in range(5)
|
|
107
|
+
]
|
|
108
|
+
count = self.monitor._count_workflow_comments(comments, "fix_comment")
|
|
109
|
+
# Should be at limit (5)
|
|
110
|
+
self.assertEqual(count, 5)
|
|
111
|
+
# 6th comment should exceed limit
|
|
112
|
+
comments.append({"body": "<!-- fix-comment-automation-commit:abc6 -->", "author": {"login": "test-automation-user"}})
|
|
113
|
+
count = self.monitor._count_workflow_comments(comments, "fix_comment")
|
|
114
|
+
self.assertEqual(count, 6)
|
|
115
|
+
self.assertGreater(count, self.monitor.safety_manager.fix_comment_limit)
|
|
116
|
+
|
|
117
|
+
def test_workflow_specific_limit_independence(self):
|
|
118
|
+
"""Test that different workflows have independent limits"""
|
|
119
|
+
# PR automation has 10 comments (at limit)
|
|
120
|
+
pr_automation_comments = [
|
|
121
|
+
{"body": f"<!-- codex-automation-commit:pr{i} -->", "author": {"login": "test-automation-user"}} for i in range(10)
|
|
122
|
+
]
|
|
123
|
+
# Fix-comment has 2 comments (under limit)
|
|
124
|
+
fix_comment_comments = [
|
|
125
|
+
{"body": f"<!-- fix-comment-automation-commit:fix{i} -->", "author": {"login": "test-automation-user"}} for i in range(2)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
pr_count = self.monitor._count_workflow_comments(pr_automation_comments, "pr_automation")
|
|
129
|
+
fix_count = self.monitor._count_workflow_comments(fix_comment_comments, "fix_comment")
|
|
130
|
+
|
|
131
|
+
self.assertEqual(pr_count, 10)
|
|
132
|
+
self.assertEqual(fix_count, 2)
|
|
133
|
+
# Fix-comment should still be allowed even though PR automation is at limit
|
|
134
|
+
self.assertLess(fix_count, self.monitor.safety_manager.fix_comment_limit)
|
|
135
|
+
self.assertEqual(pr_count, self.monitor.safety_manager.pr_automation_limit)
|
|
136
|
+
|
|
137
|
+
def test_mixed_comments_pr_automation(self):
|
|
138
|
+
"""Test PR automation counting with mixed comment types"""
|
|
139
|
+
comments = [
|
|
140
|
+
{"body": "<!-- codex-automation-commit:abc123 -->"}, # Count
|
|
141
|
+
{"body": "<!-- codex-automation-commit:def456 -->"}, # Count
|
|
142
|
+
{"body": "<!-- fix-comment-automation-commit:xyz789 -->"}, # Don't count
|
|
143
|
+
{"body": "<!-- codex-automation-commit:ghi012 -->"}, # Count
|
|
144
|
+
{"body": "Regular comment"}, # Don't count
|
|
145
|
+
]
|
|
146
|
+
count = self.monitor._count_workflow_comments(comments, "pr_automation")
|
|
147
|
+
self.assertEqual(count, 3, "Should count only codex comments without fix-comment marker")
|
|
148
|
+
|
|
149
|
+
def test_mixed_comments_fix_comment(self):
|
|
150
|
+
"""Test fix-comment counting with mixed comment types"""
|
|
151
|
+
comments = [
|
|
152
|
+
{"body": "<!-- fix-comment-automation-commit:abc123 -->", "author": {"login": "test-automation-user"}}, # Count
|
|
153
|
+
{"body": "<!-- codex-automation-commit:def456 -->", "author": {"login": "test-automation-user"}}, # Don't count
|
|
154
|
+
{"body": "<!-- fix-comment-automation-commit:xyz789 -->", "author": {"login": "test-automation-user"}}, # Count
|
|
155
|
+
{"body": "Regular comment", "author": {"login": "test-automation-user"}}, # Don't count
|
|
156
|
+
]
|
|
157
|
+
count = self.monitor._count_workflow_comments(comments, "fix_comment")
|
|
158
|
+
self.assertEqual(count, 2, "Should count only fix-comment-automation-commit comments")
|
|
159
|
+
|
|
160
|
+
def test_empty_comments_list(self):
|
|
161
|
+
"""Test counting with empty comments list"""
|
|
162
|
+
comments = []
|
|
163
|
+
for workflow_type in ["pr_automation", "fix_comment", "codex_update", "fixpr"]:
|
|
164
|
+
count = self.monitor._count_workflow_comments(comments, workflow_type)
|
|
165
|
+
self.assertEqual(count, 0, f"Empty list should return 0 for {workflow_type}")
|
|
166
|
+
|
|
167
|
+
def test_unknown_workflow_type(self):
|
|
168
|
+
"""Test that unknown workflow type falls back to counting all automation comments"""
|
|
169
|
+
comments = [
|
|
170
|
+
{"body": "<!-- codex-automation-commit:abc123 -->"},
|
|
171
|
+
{"body": "<!-- fix-comment-automation-commit:def456 -->"},
|
|
172
|
+
]
|
|
173
|
+
count = self.monitor._count_workflow_comments(comments, "unknown_workflow")
|
|
174
|
+
# Should count all automation comments as fallback
|
|
175
|
+
self.assertEqual(count, 2)
|
|
176
|
+
|
|
177
|
+
def test_count_fix_comment_ignores_impostors(self):
|
|
178
|
+
"""Test that fix-comment counts ignore comments from other users"""
|
|
179
|
+
comments = [
|
|
180
|
+
# Valid marker but wrong author
|
|
181
|
+
{"body": "<!-- fix-comment-automation-commit:abc123 -->", "author": {"login": "impostor"}},
|
|
182
|
+
# Valid marker and correct author
|
|
183
|
+
{"body": "<!-- fix-comment-automation-commit:def456 -->", "author": {"login": "test-automation-user"}},
|
|
184
|
+
]
|
|
185
|
+
count = self.monitor._count_workflow_comments(comments, "fix_comment")
|
|
186
|
+
self.assertEqual(count, 1, "Should ignore comments from impostor users")
|
|
187
|
+
|
|
188
|
+
def test_count_fixpr_ignores_impostors(self):
|
|
189
|
+
"""Test that fixpr counts ignore comments from other users"""
|
|
190
|
+
comments = [
|
|
191
|
+
# Valid marker but wrong author
|
|
192
|
+
{"body": "<!-- fixpr-run-automation-commit:gemini:abc123 -->", "author": {"login": "impostor"}},
|
|
193
|
+
# Valid marker and correct author
|
|
194
|
+
{"body": "<!-- fixpr-run-automation-commit:codex:def456 -->", "author": {"login": "test-automation-user"}},
|
|
195
|
+
]
|
|
196
|
+
count = self.monitor._count_workflow_comments(comments, "fixpr")
|
|
197
|
+
self.assertEqual(count, 1, "Should ignore comments from impostor users")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
unittest.main()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test for workspace dispatch when workspace directory doesn't exist.
|
|
4
|
+
|
|
5
|
+
This tests the critical bug where automation fails when:
|
|
6
|
+
1. PR is processed initially (workspace created)
|
|
7
|
+
2. Workspace is cleaned up
|
|
8
|
+
3. PR gets new commits
|
|
9
|
+
4. Automation tries to re-process but fails because workspace is missing
|
|
10
|
+
|
|
11
|
+
Expected behavior: Create workspace if missing, don't fail
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import unittest
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from unittest.mock import MagicMock, patch
|
|
20
|
+
|
|
21
|
+
# Import from local source, not installed package
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
23
|
+
from jleechanorg_pr_automation.orchestrated_pr_runner import prepare_workspace_dir
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestWorkspaceDispatchMissingDirectory(unittest.TestCase):
|
|
27
|
+
"""Test that workspace dispatch handles missing directories gracefully."""
|
|
28
|
+
|
|
29
|
+
def setUp(self):
|
|
30
|
+
"""Set up test workspace directory."""
|
|
31
|
+
self.test_root = Path(tempfile.mkdtemp(prefix="test_workspace_dispatch_"))
|
|
32
|
+
|
|
33
|
+
def tearDown(self):
|
|
34
|
+
"""Clean up test workspace directory."""
|
|
35
|
+
if self.test_root.exists():
|
|
36
|
+
shutil.rmtree(self.test_root)
|
|
37
|
+
|
|
38
|
+
def test_prepare_workspace_ignores_rmtree_filenotfound_race(self):
|
|
39
|
+
"""
|
|
40
|
+
Test that prepare_workspace_dir ignores FileNotFoundError from rmtree.
|
|
41
|
+
|
|
42
|
+
This can happen if the workspace exists during the initial existence check but
|
|
43
|
+
is removed concurrently (external cleanup, reboot/tmpfs cleanup, etc.) before
|
|
44
|
+
shutil.rmtree() runs.
|
|
45
|
+
"""
|
|
46
|
+
repo = "worldarchitect.ai"
|
|
47
|
+
workspace_name = "pr-2915-fix-social-hp-god-tier-enforcement"
|
|
48
|
+
|
|
49
|
+
# Workspace exists, but removal races with external cleanup.
|
|
50
|
+
workspace_path = self.test_root / repo / workspace_name
|
|
51
|
+
workspace_path.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
with patch("jleechanorg_pr_automation.orchestrated_pr_runner.WORKSPACE_ROOT_BASE", self.test_root):
|
|
54
|
+
with patch(
|
|
55
|
+
"jleechanorg_pr_automation.orchestrated_pr_runner.shutil.rmtree",
|
|
56
|
+
side_effect=FileNotFoundError("[Errno 2] No such file or directory"),
|
|
57
|
+
) as mock_rmtree:
|
|
58
|
+
result = prepare_workspace_dir(repo, workspace_name)
|
|
59
|
+
|
|
60
|
+
self.assertEqual(result, self.test_root / repo / workspace_name)
|
|
61
|
+
self.assertTrue(result.parent.exists(), f"Parent directory {result.parent} should exist after prepare_workspace_dir")
|
|
62
|
+
self.assertEqual(mock_rmtree.call_count, 1, "Expected a single rmtree attempt")
|
|
63
|
+
|
|
64
|
+
def test_prepare_workspace_raises_on_non_filenotfound_oserror(self):
|
|
65
|
+
"""
|
|
66
|
+
Test that prepare_workspace_dir still raises on serious OS errors.
|
|
67
|
+
|
|
68
|
+
Scenario: Workspace exists but rmtree fails (permissions, locks, etc.).
|
|
69
|
+
"""
|
|
70
|
+
repo = "worldarchitect.ai"
|
|
71
|
+
workspace_name = "pr-2909-fix-power-absorption-rewards-protocol"
|
|
72
|
+
|
|
73
|
+
workspace_path = self.test_root / repo / workspace_name
|
|
74
|
+
workspace_path.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
with patch("jleechanorg_pr_automation.orchestrated_pr_runner.WORKSPACE_ROOT_BASE", self.test_root):
|
|
77
|
+
with patch(
|
|
78
|
+
"jleechanorg_pr_automation.orchestrated_pr_runner.shutil.rmtree",
|
|
79
|
+
side_effect=PermissionError("[Errno 13] Permission denied"),
|
|
80
|
+
):
|
|
81
|
+
with self.assertRaises(PermissionError):
|
|
82
|
+
prepare_workspace_dir(repo, workspace_name)
|
|
83
|
+
|
|
84
|
+
def test_prepare_workspace_with_git_worktree_attempts_cleanup(self):
|
|
85
|
+
"""
|
|
86
|
+
Test that prepare_workspace_dir handles git worktrees correctly.
|
|
87
|
+
|
|
88
|
+
Scenario: Workspace is a git worktree that was cleaned up
|
|
89
|
+
Expected: Should clean up worktree metadata and proceed
|
|
90
|
+
"""
|
|
91
|
+
repo = "worldarchitect.ai"
|
|
92
|
+
workspace_name = "pr-2902-claude-test-and-fix-system-prompt-RiZyM"
|
|
93
|
+
|
|
94
|
+
# Create workspace with .git file (worktree marker)
|
|
95
|
+
workspace_path = self.test_root / repo / workspace_name
|
|
96
|
+
workspace_path.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
git_file = workspace_path / ".git"
|
|
98
|
+
git_file.write_text(
|
|
99
|
+
f"gitdir: {self.test_root / repo / '.git' / 'worktrees' / workspace_name}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
with patch("jleechanorg_pr_automation.orchestrated_pr_runner.WORKSPACE_ROOT_BASE", self.test_root):
|
|
103
|
+
with patch(
|
|
104
|
+
"jleechanorg_pr_automation.orchestrated_pr_runner.run_cmd",
|
|
105
|
+
return_value=MagicMock(returncode=0, stdout="", stderr=""),
|
|
106
|
+
) as mock_run_cmd:
|
|
107
|
+
with patch(
|
|
108
|
+
"jleechanorg_pr_automation.orchestrated_pr_runner.shutil.rmtree",
|
|
109
|
+
side_effect=FileNotFoundError("[Errno 2] No such file or directory"),
|
|
110
|
+
):
|
|
111
|
+
result = prepare_workspace_dir(repo, workspace_name)
|
|
112
|
+
|
|
113
|
+
self.assertEqual(result, self.test_root / repo / workspace_name)
|
|
114
|
+
self.assertGreaterEqual(mock_run_cmd.call_count, 2, "Should call git worktree remove and prune")
|
|
115
|
+
self.assertTrue(result.parent.exists(), "Parent directory should exist")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
unittest.main()
|
|
@@ -17,7 +17,9 @@ import tempfile
|
|
|
17
17
|
import threading
|
|
18
18
|
from datetime import datetime
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
from typing import Dict,
|
|
20
|
+
from typing import Any, Dict, Mapping, Optional
|
|
21
|
+
|
|
22
|
+
from .logging_utils import setup_logging # noqa: F401
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class SafeJSONManager:
|
|
@@ -46,7 +48,7 @@ class SafeJSONManager:
|
|
|
46
48
|
with lock:
|
|
47
49
|
try:
|
|
48
50
|
if os.path.exists(file_path):
|
|
49
|
-
with open(file_path
|
|
51
|
+
with open(file_path) as f:
|
|
50
52
|
# Add file lock for cross-process safety
|
|
51
53
|
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
|
|
52
54
|
try:
|
|
@@ -55,7 +57,7 @@ class SafeJSONManager:
|
|
|
55
57
|
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
56
58
|
else:
|
|
57
59
|
return default if default is not None else {}
|
|
58
|
-
except (json.JSONDecodeError
|
|
60
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
59
61
|
logging.warning(f"Failed to read JSON from {file_path}: {e}")
|
|
60
62
|
return default if default is not None else {}
|
|
61
63
|
|
|
@@ -78,7 +80,7 @@ class SafeJSONManager:
|
|
|
78
80
|
)
|
|
79
81
|
|
|
80
82
|
try:
|
|
81
|
-
with os.fdopen(fd,
|
|
83
|
+
with os.fdopen(fd, "w") as f:
|
|
82
84
|
# Add exclusive file lock for cross-process safety
|
|
83
85
|
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
84
86
|
try:
|
|
@@ -99,8 +101,8 @@ class SafeJSONManager:
|
|
|
99
101
|
except OSError:
|
|
100
102
|
pass
|
|
101
103
|
|
|
102
|
-
except
|
|
103
|
-
logging.
|
|
104
|
+
except OSError as e:
|
|
105
|
+
logging.exception(f"Failed to write JSON to {file_path}: {e}")
|
|
104
106
|
return False
|
|
105
107
|
|
|
106
108
|
def update_json(self, file_path: str, update_func, lock_timeout: int = 10) -> bool:
|
|
@@ -112,7 +114,7 @@ class SafeJSONManager:
|
|
|
112
114
|
updated_data = update_func(data)
|
|
113
115
|
return self.write_json(file_path, updated_data)
|
|
114
116
|
except Exception as e:
|
|
115
|
-
logging.
|
|
117
|
+
logging.exception(f"Failed to update JSON {file_path}: {e}")
|
|
116
118
|
return False
|
|
117
119
|
|
|
118
120
|
|
|
@@ -120,37 +122,6 @@ class SafeJSONManager:
|
|
|
120
122
|
json_manager = SafeJSONManager()
|
|
121
123
|
|
|
122
124
|
|
|
123
|
-
def setup_logging(name: str, level: int = logging.INFO,
|
|
124
|
-
log_file: Optional[str] = None) -> logging.Logger:
|
|
125
|
-
"""Standardized logging setup for automation components"""
|
|
126
|
-
logger = logging.getLogger(name)
|
|
127
|
-
|
|
128
|
-
# Avoid duplicate handlers
|
|
129
|
-
if logger.handlers:
|
|
130
|
-
return logger
|
|
131
|
-
|
|
132
|
-
logger.setLevel(level)
|
|
133
|
-
|
|
134
|
-
# Create formatter
|
|
135
|
-
formatter = logging.Formatter(
|
|
136
|
-
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
# Console handler
|
|
140
|
-
console_handler = logging.StreamHandler()
|
|
141
|
-
console_handler.setFormatter(formatter)
|
|
142
|
-
logger.addHandler(console_handler)
|
|
143
|
-
|
|
144
|
-
# File handler if specified
|
|
145
|
-
if log_file:
|
|
146
|
-
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
147
|
-
file_handler = logging.FileHandler(log_file)
|
|
148
|
-
file_handler.setFormatter(formatter)
|
|
149
|
-
logger.addHandler(file_handler)
|
|
150
|
-
|
|
151
|
-
return logger
|
|
152
|
-
|
|
153
|
-
|
|
154
125
|
def get_env_config(prefix: str = "AUTOMATION_") -> Dict[str, str]:
|
|
155
126
|
"""Get all environment variables with specified prefix"""
|
|
156
127
|
config = {}
|
|
@@ -165,31 +136,85 @@ def get_env_config(prefix: str = "AUTOMATION_") -> Dict[str, str]:
|
|
|
165
136
|
def get_email_config() -> Dict[str, str]:
|
|
166
137
|
"""Get email configuration from environment variables"""
|
|
167
138
|
email_config = {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
139
|
+
"smtp_server": os.getenv("SMTP_SERVER", "localhost"),
|
|
140
|
+
"smtp_port": int(os.getenv("SMTP_PORT", "587")),
|
|
141
|
+
"email_user": os.getenv("EMAIL_USER", ""),
|
|
142
|
+
"email_pass": os.getenv("EMAIL_PASS", ""),
|
|
143
|
+
"email_to": os.getenv("EMAIL_TO", ""),
|
|
144
|
+
"email_from": os.getenv("EMAIL_FROM", os.getenv("EMAIL_USER", ""))
|
|
174
145
|
}
|
|
175
146
|
return email_config
|
|
176
147
|
|
|
177
148
|
|
|
178
149
|
def validate_email_config(config: Dict[str, str]) -> bool:
|
|
179
150
|
"""Validate that required email configuration is present"""
|
|
180
|
-
required_fields = [
|
|
151
|
+
required_fields = ["smtp_server", "email_user", "email_pass", "email_to"]
|
|
181
152
|
return all(config.get(field) for field in required_fields)
|
|
182
153
|
|
|
183
154
|
|
|
184
155
|
def get_automation_limits() -> Dict[str, int]:
|
|
185
|
-
"""Get automation safety limits from
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
156
|
+
"""Get automation safety limits from defaults with optional overrides.
|
|
157
|
+
|
|
158
|
+
Supports workflow-specific limits:
|
|
159
|
+
- pr_automation: Default PR monitoring workflow (posts codex comments)
|
|
160
|
+
- fix_comment: Fix-comment workflow (addresses review comments)
|
|
161
|
+
- codex_update: Codex update workflow (browser automation)
|
|
162
|
+
- fixpr: FixPR workflow (resolves conflicts/failing checks)
|
|
163
|
+
"""
|
|
164
|
+
return get_automation_limits_with_overrides()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def coerce_positive_int(value: Any, *, default: int) -> int:
|
|
168
|
+
try:
|
|
169
|
+
parsed = int(value)
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
return default
|
|
172
|
+
return parsed if parsed > 0 else default
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_automation_limits_with_overrides(overrides: Optional[Mapping[str, Any]] = None) -> Dict[str, int]:
|
|
176
|
+
"""Internal helper to keep defaults centralized and overrides explicit.
|
|
177
|
+
|
|
178
|
+
New limit structure:
|
|
179
|
+
- pr_limit: 50 total attempts across ALL workflows for a PR
|
|
180
|
+
- workflow limits: 10 attempts per workflow (counts ALL attempts, not just failures)
|
|
181
|
+
"""
|
|
182
|
+
# Global PR limit: 50 total attempts across all workflows
|
|
183
|
+
pr_limit_default = 50
|
|
184
|
+
pr_limit = coerce_positive_int(os.getenv("AUTOMATION_PR_LIMIT"), default=pr_limit_default)
|
|
185
|
+
|
|
186
|
+
# Per-workflow limit: 10 attempts per workflow
|
|
187
|
+
workflow_limit_default = 10
|
|
188
|
+
|
|
189
|
+
defaults: Dict[str, int] = {
|
|
190
|
+
# Global PR limit: counts ALL attempts across ALL workflows
|
|
191
|
+
"pr_limit": pr_limit,
|
|
192
|
+
"global_limit": coerce_positive_int(os.getenv("AUTOMATION_GLOBAL_LIMIT"), default=50),
|
|
193
|
+
"approval_hours": 24,
|
|
194
|
+
"subprocess_timeout": 300,
|
|
195
|
+
# Workflow-specific limits: 10 attempts per workflow (counts ALL attempts)
|
|
196
|
+
"pr_automation_limit": coerce_positive_int(
|
|
197
|
+
os.getenv("AUTOMATION_PR_AUTOMATION_LIMIT"), default=workflow_limit_default
|
|
198
|
+
),
|
|
199
|
+
"fix_comment_limit": coerce_positive_int(
|
|
200
|
+
os.getenv("AUTOMATION_FIX_COMMENT_LIMIT"), default=workflow_limit_default
|
|
201
|
+
),
|
|
202
|
+
"codex_update_limit": coerce_positive_int(
|
|
203
|
+
os.getenv("AUTOMATION_CODEX_UPDATE_LIMIT"), default=workflow_limit_default
|
|
204
|
+
),
|
|
205
|
+
"fixpr_limit": coerce_positive_int(os.getenv("AUTOMATION_FIXPR_LIMIT"), default=workflow_limit_default),
|
|
191
206
|
}
|
|
192
207
|
|
|
208
|
+
if not overrides:
|
|
209
|
+
return dict(defaults)
|
|
210
|
+
|
|
211
|
+
limits = dict(defaults)
|
|
212
|
+
for key in list(defaults.keys()):
|
|
213
|
+
if key in overrides:
|
|
214
|
+
limits[key] = coerce_positive_int(overrides.get(key), default=defaults[key])
|
|
215
|
+
|
|
216
|
+
return limits
|
|
217
|
+
|
|
193
218
|
|
|
194
219
|
def ensure_directory(file_path: str) -> None:
|
|
195
220
|
"""Ensure directory exists for given file path"""
|
|
@@ -224,9 +249,9 @@ def parse_timestamp(timestamp_str: str) -> datetime:
|
|
|
224
249
|
def get_test_email_config() -> Dict[str, str]:
|
|
225
250
|
"""Get standardized test email configuration"""
|
|
226
251
|
return {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
252
|
+
"SMTP_SERVER": "smtp.example.com",
|
|
253
|
+
"SMTP_PORT": "587",
|
|
254
|
+
"EMAIL_USER": "test@example.com",
|
|
255
|
+
"EMAIL_PASS": "testpass",
|
|
256
|
+
"EMAIL_TO": "admin@example.com"
|
|
232
257
|
}
|