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,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for fixpr prompt formatting."""
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from automation.jleechanorg_pr_automation import orchestrated_pr_runner as runner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _FakeDispatcher:
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self.task_description = None
|
|
15
|
+
|
|
16
|
+
def analyze_task_and_create_agents(self, task_description: str, forced_cli: str = "claude"):
|
|
17
|
+
self.task_description = task_description
|
|
18
|
+
return [{"name": "test-agent"}]
|
|
19
|
+
|
|
20
|
+
def create_dynamic_agent(self, agent_spec): # pragma: no cover - simple stub
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestFixprPrompt(unittest.TestCase):
|
|
25
|
+
def test_fixpr_commit_message_includes_mode_and_model(self):
|
|
26
|
+
pr_payload = {
|
|
27
|
+
"repo_full": "jleechanorg/worldarchitect.ai",
|
|
28
|
+
"repo": "worldarchitect.ai",
|
|
29
|
+
"number": 123,
|
|
30
|
+
"title": "Test PR",
|
|
31
|
+
"branch": "feature/test-fixpr",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dispatcher = _FakeDispatcher()
|
|
35
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
36
|
+
with patch.object(runner, "WORKSPACE_ROOT_BASE", Path(tmpdir)):
|
|
37
|
+
with patch.object(runner, "kill_tmux_session_if_exists", lambda _: None):
|
|
38
|
+
ok = runner.dispatch_agent_for_pr(dispatcher, pr_payload, agent_cli="codex")
|
|
39
|
+
|
|
40
|
+
self.assertTrue(ok)
|
|
41
|
+
self.assertIsNotNone(dispatcher.task_description)
|
|
42
|
+
self.assertIn(
|
|
43
|
+
"[fixpr codex-automation-commit] fix PR #123",
|
|
44
|
+
dispatcher.task_description,
|
|
45
|
+
)
|
|
46
|
+
# Verify the prompt instructs fetching ALL feedback sources (inline review comments included).
|
|
47
|
+
self.assertIn("/pulls/{pr}/comments", dispatcher.task_description)
|
|
48
|
+
self.assertIn("/pulls/{pr}/reviews", dispatcher.task_description)
|
|
49
|
+
self.assertIn("/issues/{pr}/comments", dispatcher.task_description)
|
|
50
|
+
self.assertIn("--paginate", dispatcher.task_description)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
unittest.main()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test for fixpr workflow return value handling.
|
|
4
|
+
|
|
5
|
+
Regression test for cursor[bot] bug report (Comment ID 2674134633):
|
|
6
|
+
"FixPR workflow ignores queued comment posting failure"
|
|
7
|
+
|
|
8
|
+
Tests that _process_pr_fixpr correctly captures and handles the return value
|
|
9
|
+
from _post_fixpr_queued, matching the behavior of _process_pr_fix_comment.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import unittest
|
|
13
|
+
from unittest.mock import Mock, patch, MagicMock, call
|
|
14
|
+
|
|
15
|
+
from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestFixprReturnValue(unittest.TestCase):
|
|
19
|
+
"""Test fixpr workflow return value handling"""
|
|
20
|
+
|
|
21
|
+
def setUp(self):
|
|
22
|
+
"""Set up test environment with comprehensive mocking"""
|
|
23
|
+
# Patch AutomationSafetyManager during JleechanorgPRMonitor initialization
|
|
24
|
+
with patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.AutomationSafetyManager'):
|
|
25
|
+
self.monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
26
|
+
self.monitor.safety_manager.fixpr_limit = 10
|
|
27
|
+
|
|
28
|
+
# Mock logger to avoid logging issues
|
|
29
|
+
self.monitor.logger = MagicMock()
|
|
30
|
+
|
|
31
|
+
@patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.dispatch_agent_for_pr')
|
|
32
|
+
@patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.ensure_base_clone')
|
|
33
|
+
@patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.chdir')
|
|
34
|
+
@patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.TaskDispatcher')
|
|
35
|
+
def test_fixpr_returns_partial_when_queued_comment_fails(
|
|
36
|
+
self,
|
|
37
|
+
mock_dispatcher,
|
|
38
|
+
mock_chdir,
|
|
39
|
+
mock_clone,
|
|
40
|
+
mock_dispatch_agent,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Test that _process_pr_fixpr returns 'partial' when _post_fixpr_queued fails.
|
|
44
|
+
|
|
45
|
+
Regression test for cursor[bot] bug: Method was ignoring return value and
|
|
46
|
+
always returning 'posted' even when comment posting failed.
|
|
47
|
+
"""
|
|
48
|
+
# Setup mocks
|
|
49
|
+
mock_clone.return_value = "/tmp/fake/repo"
|
|
50
|
+
mock_dispatch_agent.return_value = True # Agent dispatch succeeds
|
|
51
|
+
|
|
52
|
+
pr_data = {
|
|
53
|
+
"number": 1234,
|
|
54
|
+
"title": "Test PR",
|
|
55
|
+
"headRefName": "test-branch",
|
|
56
|
+
"baseRefName": "main",
|
|
57
|
+
"url": "https://github.com/test/repo/pull/1234",
|
|
58
|
+
"headRepository": {"owner": {"login": "test"}},
|
|
59
|
+
"headRefOid": "abc123",
|
|
60
|
+
"statusCheckRollup": [],
|
|
61
|
+
"mergeable": "MERGEABLE",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Comprehensive mocking to avoid all side effects
|
|
65
|
+
with patch.object(self.monitor, '_normalize_repository_name', return_value="test/repo"):
|
|
66
|
+
with patch.object(self.monitor, '_get_pr_comment_state', return_value=("abc123", [])):
|
|
67
|
+
with patch.object(self.monitor, '_should_skip_pr', return_value=False):
|
|
68
|
+
with patch.object(self.monitor, '_count_workflow_comments', return_value=5): # Under limit
|
|
69
|
+
with patch.object(self.monitor, '_post_fixpr_queued', return_value=False) as mock_post_queued: # FAILS
|
|
70
|
+
with patch.object(self.monitor, '_record_processed_pr'):
|
|
71
|
+
result = self.monitor._process_pr_fixpr(
|
|
72
|
+
repository="test/repo",
|
|
73
|
+
pr_number=1234,
|
|
74
|
+
pr_data=pr_data,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# CRITICAL: Should return "partial" when queued comment fails
|
|
78
|
+
self.assertEqual(
|
|
79
|
+
result,
|
|
80
|
+
"partial",
|
|
81
|
+
"REGRESSION BUG: _process_pr_fixpr should return 'partial' when _post_fixpr_queued fails, "
|
|
82
|
+
"not ignore the return value. This causes failed marker posts to not count against fixpr_limit."
|
|
83
|
+
)
|
|
84
|
+
# Verify _post_fixpr_queued was called
|
|
85
|
+
mock_post_queued.assert_called_once()
|
|
86
|
+
|
|
87
|
+
@patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.dispatch_agent_for_pr')
|
|
88
|
+
@patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.ensure_base_clone')
|
|
89
|
+
@patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.chdir')
|
|
90
|
+
@patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.TaskDispatcher')
|
|
91
|
+
def test_fixpr_returns_posted_when_queued_comment_succeeds(
|
|
92
|
+
self,
|
|
93
|
+
mock_dispatcher,
|
|
94
|
+
mock_chdir,
|
|
95
|
+
mock_clone,
|
|
96
|
+
mock_dispatch_agent,
|
|
97
|
+
):
|
|
98
|
+
"""
|
|
99
|
+
Test that _process_pr_fixpr returns 'posted' when _post_fixpr_queued succeeds.
|
|
100
|
+
|
|
101
|
+
This is the happy path - verifies correct behavior when comment posting works.
|
|
102
|
+
"""
|
|
103
|
+
# Setup mocks
|
|
104
|
+
mock_clone.return_value = "/tmp/fake/repo"
|
|
105
|
+
mock_dispatch_agent.return_value = True # Agent dispatch succeeds
|
|
106
|
+
|
|
107
|
+
pr_data = {
|
|
108
|
+
"number": 1234,
|
|
109
|
+
"title": "Test PR",
|
|
110
|
+
"headRefName": "test-branch",
|
|
111
|
+
"baseRefName": "main",
|
|
112
|
+
"url": "https://github.com/test/repo/pull/1234",
|
|
113
|
+
"headRepository": {"owner": {"login": "test"}},
|
|
114
|
+
"headRefOid": "abc123",
|
|
115
|
+
"statusCheckRollup": [],
|
|
116
|
+
"mergeable": "MERGEABLE",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Comprehensive mocking to avoid all side effects
|
|
120
|
+
with patch.object(self.monitor, '_normalize_repository_name', return_value="test/repo"):
|
|
121
|
+
with patch.object(self.monitor, '_get_pr_comment_state', return_value=("abc123", [])):
|
|
122
|
+
with patch.object(self.monitor, '_should_skip_pr', return_value=False):
|
|
123
|
+
with patch.object(self.monitor, '_count_workflow_comments', return_value=5): # Under limit
|
|
124
|
+
with patch.object(self.monitor, '_post_fixpr_queued', return_value=True) as mock_post_queued: # SUCCEEDS
|
|
125
|
+
with patch.object(self.monitor, '_record_processed_pr'):
|
|
126
|
+
result = self.monitor._process_pr_fixpr(
|
|
127
|
+
repository="test/repo",
|
|
128
|
+
pr_number=1234,
|
|
129
|
+
pr_data=pr_data,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Should return "posted" when everything succeeds
|
|
133
|
+
self.assertEqual(result, "posted")
|
|
134
|
+
# Verify _post_fixpr_queued was called
|
|
135
|
+
mock_post_queued.assert_called_once()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
unittest.main()
|
|
@@ -13,51 +13,51 @@ class TestGraphQLErrorHandling(unittest.TestCase):
|
|
|
13
13
|
"""Validate robust error handling for GraphQL API failures."""
|
|
14
14
|
|
|
15
15
|
def setUp(self) -> None:
|
|
16
|
-
self.monitor = JleechanorgPRMonitor()
|
|
16
|
+
self.monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
17
17
|
|
|
18
|
-
@patch(
|
|
18
|
+
@patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout")
|
|
19
19
|
def test_handles_api_timeout(self, mock_exec) -> None:
|
|
20
20
|
"""Should return None when GraphQL API times out"""
|
|
21
|
-
mock_exec.side_effect = subprocess.TimeoutExpired([
|
|
21
|
+
mock_exec.side_effect = subprocess.TimeoutExpired(["gh"], 30)
|
|
22
22
|
|
|
23
|
-
result = self.monitor._get_head_commit_details(
|
|
23
|
+
result = self.monitor._get_head_commit_details("org/repo", 123)
|
|
24
24
|
|
|
25
25
|
self.assertIsNone(result, "Should return None on API timeout")
|
|
26
26
|
|
|
27
|
-
@patch(
|
|
27
|
+
@patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout")
|
|
28
28
|
def test_handles_malformed_json(self, mock_exec) -> None:
|
|
29
29
|
"""Should return None when GraphQL returns invalid JSON"""
|
|
30
30
|
mock_exec.return_value = MagicMock(
|
|
31
31
|
stdout='{"invalid": json, missing quotes}'
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
result = self.monitor._get_head_commit_details(
|
|
34
|
+
result = self.monitor._get_head_commit_details("org/repo", 123)
|
|
35
35
|
|
|
36
36
|
self.assertIsNone(result, "Should return None on malformed JSON")
|
|
37
37
|
|
|
38
|
-
@patch(
|
|
38
|
+
@patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout")
|
|
39
39
|
def test_handles_missing_data_field(self, mock_exec) -> None:
|
|
40
40
|
"""Should handle missing 'data' field in GraphQL response"""
|
|
41
41
|
mock_exec.return_value = MagicMock(
|
|
42
42
|
stdout='{"errors": [{"message": "Field error"}]}'
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
result = self.monitor._get_head_commit_details(
|
|
45
|
+
result = self.monitor._get_head_commit_details("org/repo", 123)
|
|
46
46
|
|
|
47
47
|
self.assertIsNone(result, "Should return None when data field missing")
|
|
48
48
|
|
|
49
|
-
@patch(
|
|
49
|
+
@patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout")
|
|
50
50
|
def test_handles_missing_repository_field(self, mock_exec) -> None:
|
|
51
51
|
"""Should handle missing 'repository' field gracefully"""
|
|
52
52
|
mock_exec.return_value = MagicMock(
|
|
53
53
|
stdout='{"data": {}}'
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
-
result = self.monitor._get_head_commit_details(
|
|
56
|
+
result = self.monitor._get_head_commit_details("org/repo", 123)
|
|
57
57
|
|
|
58
58
|
self.assertIsNone(result, "Should return None when repository field missing")
|
|
59
59
|
|
|
60
|
-
@patch(
|
|
60
|
+
@patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout")
|
|
61
61
|
def test_handles_missing_commits(self, mock_exec) -> None:
|
|
62
62
|
"""Should handle missing commits array gracefully"""
|
|
63
63
|
response = {
|
|
@@ -69,11 +69,11 @@ class TestGraphQLErrorHandling(unittest.TestCase):
|
|
|
69
69
|
}
|
|
70
70
|
mock_exec.return_value = MagicMock(stdout=json.dumps(response))
|
|
71
71
|
|
|
72
|
-
result = self.monitor._get_head_commit_details(
|
|
72
|
+
result = self.monitor._get_head_commit_details("org/repo", 123)
|
|
73
73
|
|
|
74
74
|
self.assertIsNone(result, "Should return None when commits missing")
|
|
75
75
|
|
|
76
|
-
@patch(
|
|
76
|
+
@patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout")
|
|
77
77
|
def test_handles_empty_commits_array(self, mock_exec) -> None:
|
|
78
78
|
"""Should handle empty commits array gracefully"""
|
|
79
79
|
response = {
|
|
@@ -87,66 +87,66 @@ class TestGraphQLErrorHandling(unittest.TestCase):
|
|
|
87
87
|
}
|
|
88
88
|
mock_exec.return_value = MagicMock(stdout=json.dumps(response))
|
|
89
89
|
|
|
90
|
-
result = self.monitor._get_head_commit_details(
|
|
90
|
+
result = self.monitor._get_head_commit_details("org/repo", 123)
|
|
91
91
|
|
|
92
92
|
self.assertIsNone(result, "Should return None when commits array empty")
|
|
93
93
|
|
|
94
|
-
@patch(
|
|
94
|
+
@patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout")
|
|
95
95
|
def test_handles_called_process_error(self, mock_exec) -> None:
|
|
96
96
|
"""Should handle subprocess CalledProcessError gracefully"""
|
|
97
97
|
mock_exec.side_effect = subprocess.CalledProcessError(
|
|
98
98
|
returncode=1,
|
|
99
|
-
cmd=[
|
|
100
|
-
stderr=
|
|
99
|
+
cmd=["gh", "api"],
|
|
100
|
+
stderr="API rate limit exceeded"
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
-
result = self.monitor._get_head_commit_details(
|
|
103
|
+
result = self.monitor._get_head_commit_details("org/repo", 123)
|
|
104
104
|
|
|
105
105
|
self.assertIsNone(result, "Should return None on CalledProcessError")
|
|
106
106
|
|
|
107
|
-
@patch(
|
|
107
|
+
@patch("jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout")
|
|
108
108
|
def test_handles_generic_exception(self, mock_exec) -> None:
|
|
109
109
|
"""Should handle unexpected exceptions gracefully"""
|
|
110
110
|
mock_exec.side_effect = RuntimeError("Unexpected error")
|
|
111
111
|
|
|
112
|
-
result = self.monitor._get_head_commit_details(
|
|
112
|
+
result = self.monitor._get_head_commit_details("org/repo", 123)
|
|
113
113
|
|
|
114
114
|
self.assertIsNone(result, "Should return None on unexpected exception")
|
|
115
115
|
|
|
116
116
|
def test_validates_invalid_repo_format(self) -> None:
|
|
117
117
|
"""Should return None for invalid repository format"""
|
|
118
|
-
result = self.monitor._get_head_commit_details(
|
|
118
|
+
result = self.monitor._get_head_commit_details("invalid-no-slash", 123)
|
|
119
119
|
|
|
120
120
|
self.assertIsNone(result, "Should reject repo without slash separator")
|
|
121
121
|
|
|
122
122
|
def test_validates_empty_repo_name(self) -> None:
|
|
123
123
|
"""Should return None for empty repository parts"""
|
|
124
|
-
result = self.monitor._get_head_commit_details(
|
|
124
|
+
result = self.monitor._get_head_commit_details("/repo", 123)
|
|
125
125
|
|
|
126
126
|
self.assertIsNone(result, "Should reject empty owner")
|
|
127
127
|
|
|
128
128
|
def test_validates_invalid_github_owner_name(self) -> None:
|
|
129
129
|
"""Should return None for invalid GitHub owner/repo names"""
|
|
130
130
|
# GitHub names cannot start with hyphen
|
|
131
|
-
result = self.monitor._get_head_commit_details(
|
|
131
|
+
result = self.monitor._get_head_commit_details("-invalid/repo", 123)
|
|
132
132
|
|
|
133
133
|
self.assertIsNone(result, "Should reject owner starting with hyphen")
|
|
134
134
|
|
|
135
135
|
def test_validates_invalid_pr_number_string(self) -> None:
|
|
136
136
|
"""Should return None for non-integer PR number"""
|
|
137
|
-
result = self.monitor._get_head_commit_details(
|
|
137
|
+
result = self.monitor._get_head_commit_details("org/repo", "not-a-number")
|
|
138
138
|
|
|
139
139
|
self.assertIsNone(result, "Should reject string PR number")
|
|
140
140
|
|
|
141
141
|
def test_validates_negative_pr_number(self) -> None:
|
|
142
142
|
"""Should return None for negative PR number"""
|
|
143
|
-
result = self.monitor._get_head_commit_details(
|
|
143
|
+
result = self.monitor._get_head_commit_details("org/repo", -1)
|
|
144
144
|
|
|
145
145
|
self.assertIsNone(result, "Should reject negative PR number")
|
|
146
146
|
|
|
147
147
|
def test_validates_zero_pr_number(self) -> None:
|
|
148
148
|
"""Should return None for zero PR number"""
|
|
149
|
-
result = self.monitor._get_head_commit_details(
|
|
149
|
+
result = self.monitor._get_head_commit_details("org/repo", 0)
|
|
150
150
|
|
|
151
151
|
self.assertIsNone(result, "Should reject zero PR number")
|
|
152
152
|
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for --model parameter support in orchestration and automation.
|
|
4
|
+
|
|
5
|
+
Validates that model parameter is correctly passed through the automation pipeline.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import unittest
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
|
|
13
|
+
from jleechanorg_pr_automation.jleechanorg_pr_monitor import (
|
|
14
|
+
JleechanorgPRMonitor,
|
|
15
|
+
_normalize_model,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestModelParameter(unittest.TestCase):
|
|
20
|
+
"""Test suite for model parameter functionality."""
|
|
21
|
+
|
|
22
|
+
def setUp(self):
|
|
23
|
+
"""Set up test fixtures."""
|
|
24
|
+
self.monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
25
|
+
|
|
26
|
+
def test_process_single_pr_accepts_model_parameter(self):
|
|
27
|
+
"""Test that process_single_pr_by_number accepts model parameter."""
|
|
28
|
+
with patch.object(self.monitor, 'safety_manager') as mock_safety:
|
|
29
|
+
with patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.AutomationUtils'):
|
|
30
|
+
mock_safety.can_start_global_run.return_value = True
|
|
31
|
+
mock_safety.try_process_pr.return_value = True
|
|
32
|
+
|
|
33
|
+
# Should not raise TypeError for model parameter
|
|
34
|
+
try:
|
|
35
|
+
self.monitor.process_single_pr_by_number(
|
|
36
|
+
pr_number=1234,
|
|
37
|
+
repository="test/repo",
|
|
38
|
+
fix_comment=True,
|
|
39
|
+
agent_cli="claude",
|
|
40
|
+
model="sonnet" # Test model parameter
|
|
41
|
+
)
|
|
42
|
+
except TypeError as e:
|
|
43
|
+
if "model" in str(e):
|
|
44
|
+
self.fail(f"process_single_pr_by_number does not accept model parameter: {e}")
|
|
45
|
+
except Exception:
|
|
46
|
+
# Other exceptions are okay for this test - we just want to verify signature
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def test_dispatch_fix_comment_agent_accepts_model_parameter(self):
|
|
50
|
+
"""Test that dispatch_fix_comment_agent accepts model parameter."""
|
|
51
|
+
with patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.ensure_base_clone') as mock_clone, patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.chdir'), patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.TaskDispatcher'), patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.dispatch_agent_for_pr_with_task') as mock_dispatch:
|
|
52
|
+
|
|
53
|
+
mock_clone.return_value = "/tmp/fake/repo"
|
|
54
|
+
mock_dispatch.return_value = True
|
|
55
|
+
|
|
56
|
+
pr_data = {
|
|
57
|
+
"number": 1234,
|
|
58
|
+
"title": "Test PR",
|
|
59
|
+
"headRefName": "test-branch",
|
|
60
|
+
"url": "https://github.com/test/repo/pull/1234"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Should not raise TypeError for model parameter
|
|
64
|
+
try:
|
|
65
|
+
self.monitor.dispatch_fix_comment_agent(
|
|
66
|
+
repository="test/repo",
|
|
67
|
+
pr_number=1234,
|
|
68
|
+
pr_data=pr_data,
|
|
69
|
+
agent_cli="claude",
|
|
70
|
+
model="opus" # Test model parameter
|
|
71
|
+
)
|
|
72
|
+
# Verify it was passed through
|
|
73
|
+
mock_dispatch.assert_called_once()
|
|
74
|
+
call_kwargs = mock_dispatch.call_args[1]
|
|
75
|
+
self.assertEqual(call_kwargs.get("model"), "opus")
|
|
76
|
+
except TypeError as e:
|
|
77
|
+
if "model" in str(e):
|
|
78
|
+
self.fail(f"dispatch_fix_comment_agent does not accept model parameter: {e}")
|
|
79
|
+
|
|
80
|
+
def test_model_parameter_passed_to_dispatcher(self):
|
|
81
|
+
"""Test that model parameter is passed through to dispatcher."""
|
|
82
|
+
with patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.TaskDispatcher'), patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.ensure_base_clone'), patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.chdir'), patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.dispatch_agent_for_pr_with_task') as mock_dispatch, patch.object(self.monitor, '_post_fix_comment_queued') as mock_queued, patch.object(self.monitor, '_start_fix_comment_review_watcher') as mock_watcher, patch.object(self.monitor, '_get_pr_comment_state', return_value=(None, [])):
|
|
83
|
+
|
|
84
|
+
mock_dispatch.return_value = True
|
|
85
|
+
mock_queued.return_value = True
|
|
86
|
+
mock_watcher.return_value = True
|
|
87
|
+
|
|
88
|
+
pr_data = {
|
|
89
|
+
"number": 1234,
|
|
90
|
+
"title": "Test PR",
|
|
91
|
+
"headRefName": "test-branch",
|
|
92
|
+
"baseRefName": "main",
|
|
93
|
+
"url": "https://github.com/test/repo/pull/1234",
|
|
94
|
+
"headRepository": {"owner": {"login": "test"}},
|
|
95
|
+
"headRefOid": "abc123"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Process fix-comment with model parameter
|
|
99
|
+
self.monitor._process_pr_fix_comment(
|
|
100
|
+
repository="test/repo",
|
|
101
|
+
pr_number=1234,
|
|
102
|
+
pr_data=pr_data,
|
|
103
|
+
agent_cli="claude",
|
|
104
|
+
model="haiku" # Test model parameter
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Verify dispatcher was called with model parameter
|
|
108
|
+
mock_dispatch.assert_called_once()
|
|
109
|
+
call_kwargs = mock_dispatch.call_args[1]
|
|
110
|
+
self.assertEqual(call_kwargs.get("model"), "haiku")
|
|
111
|
+
|
|
112
|
+
def test_cli_argument_parser_has_model_flag(self):
|
|
113
|
+
"""Test that CLI argument parser includes --model flag."""
|
|
114
|
+
# Get the argument parser from the module
|
|
115
|
+
parser = argparse.ArgumentParser()
|
|
116
|
+
|
|
117
|
+
# Add the expected arguments (mimicking what main() does)
|
|
118
|
+
parser.add_argument("--model", type=str, default=None,
|
|
119
|
+
help="Model to use for Claude CLI")
|
|
120
|
+
|
|
121
|
+
# Should not raise error
|
|
122
|
+
args = parser.parse_args(["--model", "sonnet"])
|
|
123
|
+
self.assertEqual(args.model, "sonnet")
|
|
124
|
+
|
|
125
|
+
def test_model_defaults_to_none(self):
|
|
126
|
+
"""Test that model parameter defaults to None when not provided."""
|
|
127
|
+
parser = argparse.ArgumentParser()
|
|
128
|
+
parser.add_argument("--model", type=str, default=None)
|
|
129
|
+
|
|
130
|
+
# Parse without --model flag
|
|
131
|
+
args = parser.parse_args([])
|
|
132
|
+
self.assertIsNone(args.model)
|
|
133
|
+
|
|
134
|
+
def test_multiple_model_values(self):
|
|
135
|
+
"""Test different valid model values."""
|
|
136
|
+
valid_models = ["sonnet", "opus", "haiku", "gemini-3-pro-preview", "composer-1"]
|
|
137
|
+
|
|
138
|
+
for model in valid_models:
|
|
139
|
+
with self.subTest(model=model):
|
|
140
|
+
with patch.object(self.monitor, 'safety_manager') as mock_safety:
|
|
141
|
+
with patch('jleechanorg_pr_automation.jleechanorg_pr_monitor.AutomationUtils'):
|
|
142
|
+
mock_safety.can_start_global_run.return_value = True
|
|
143
|
+
mock_safety.try_process_pr.return_value = True
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
self.monitor.process_single_pr_by_number(
|
|
147
|
+
pr_number=1234,
|
|
148
|
+
repository="test/repo",
|
|
149
|
+
fix_comment=True,
|
|
150
|
+
agent_cli="claude",
|
|
151
|
+
model=model
|
|
152
|
+
)
|
|
153
|
+
except TypeError as e:
|
|
154
|
+
if "model" in str(e):
|
|
155
|
+
self.fail(f"Model parameter not accepted for value '{model}': {e}")
|
|
156
|
+
except Exception:
|
|
157
|
+
# Other exceptions are okay
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
def test_fixpr_run_monitoring_cycle_threads_model(self):
|
|
161
|
+
"""FixPR mode should pass --model through to _process_pr_fixpr."""
|
|
162
|
+
monitor = JleechanorgPRMonitor()
|
|
163
|
+
pr = {
|
|
164
|
+
"repository": "test/repo",
|
|
165
|
+
"repositoryFullName": "test/repo",
|
|
166
|
+
"number": 123,
|
|
167
|
+
"title": "Test PR",
|
|
168
|
+
"headRefName": "feature/test",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
with patch.object(monitor, "discover_open_prs", return_value=[pr]), \
|
|
172
|
+
patch.object(monitor, "is_pr_actionable", return_value=True), \
|
|
173
|
+
patch.object(monitor, "_get_pr_comment_state", return_value=(None, [])), \
|
|
174
|
+
patch.object(monitor, "_process_pr_fixpr", return_value="skipped") as mock_fixpr, \
|
|
175
|
+
patch("jleechanorg_pr_automation.jleechanorg_pr_monitor.has_failing_checks", return_value=True), \
|
|
176
|
+
patch("jleechanorg_pr_automation.jleechanorg_pr_monitor.AutomationUtils.execute_subprocess_with_timeout",
|
|
177
|
+
return_value=SimpleNamespace(returncode=0, stdout='{\"mergeable\":\"MERGEABLE\"}')):
|
|
178
|
+
|
|
179
|
+
with patch.object(monitor, "safety_manager") as mock_safety:
|
|
180
|
+
mock_safety.can_start_global_run.return_value = True
|
|
181
|
+
mock_safety.try_process_pr.return_value = True
|
|
182
|
+
mock_safety.get_global_runs.return_value = 1
|
|
183
|
+
mock_safety.global_limit = 50
|
|
184
|
+
mock_safety.fixpr_limit = 10
|
|
185
|
+
mock_safety.pr_limit = 10
|
|
186
|
+
mock_safety.pr_automation_limit = 10
|
|
187
|
+
mock_safety.fix_comment_limit = 10
|
|
188
|
+
|
|
189
|
+
monitor.run_monitoring_cycle(
|
|
190
|
+
max_prs=1,
|
|
191
|
+
cutoff_hours=24,
|
|
192
|
+
fixpr=True,
|
|
193
|
+
agent_cli="claude",
|
|
194
|
+
model="sonnet",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
self.assertTrue(mock_fixpr.called)
|
|
198
|
+
self.assertEqual(mock_fixpr.call_args[1].get("model"), "sonnet")
|
|
199
|
+
|
|
200
|
+
def test_fixpr_process_pr_threads_model_to_dispatch(self):
|
|
201
|
+
"""_process_pr_fixpr should forward model through to dispatch_agent_for_pr."""
|
|
202
|
+
monitor = JleechanorgPRMonitor()
|
|
203
|
+
pr_data = {
|
|
204
|
+
"number": 123,
|
|
205
|
+
"title": "Test PR",
|
|
206
|
+
"headRefName": "feature/test",
|
|
207
|
+
"url": "https://github.com/test/repo/pull/123",
|
|
208
|
+
"headRefOid": "abc123",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
with patch.object(monitor, "_get_pr_comment_state", return_value=(None, [])), \
|
|
212
|
+
patch.object(monitor, "_should_skip_pr", return_value=False), \
|
|
213
|
+
patch.object(monitor, "_post_fixpr_queued", return_value=True), \
|
|
214
|
+
patch("jleechanorg_pr_automation.jleechanorg_pr_monitor.ensure_base_clone", return_value="/tmp/fake/repo"), \
|
|
215
|
+
patch("jleechanorg_pr_automation.jleechanorg_pr_monitor.chdir"), \
|
|
216
|
+
patch("jleechanorg_pr_automation.jleechanorg_pr_monitor.TaskDispatcher"), \
|
|
217
|
+
patch("jleechanorg_pr_automation.jleechanorg_pr_monitor.dispatch_agent_for_pr", return_value=True) as mock_dispatch:
|
|
218
|
+
|
|
219
|
+
monitor.safety_manager.fixpr_limit = 10
|
|
220
|
+
result = monitor._process_pr_fixpr(
|
|
221
|
+
repository="test/repo",
|
|
222
|
+
pr_number=123,
|
|
223
|
+
pr_data=pr_data,
|
|
224
|
+
agent_cli="claude",
|
|
225
|
+
model="sonnet",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
self.assertEqual(result, "posted")
|
|
229
|
+
self.assertEqual(mock_dispatch.call_args[1].get("model"), "sonnet")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_normalize_model_none_returns_none(self):
|
|
233
|
+
"""Test that _normalize_model returns None for None input."""
|
|
234
|
+
result = _normalize_model(None)
|
|
235
|
+
self.assertIsNone(result)
|
|
236
|
+
|
|
237
|
+
def test_normalize_model_empty_string_returns_none(self):
|
|
238
|
+
"""Test that _normalize_model returns None for empty string."""
|
|
239
|
+
result = _normalize_model("")
|
|
240
|
+
self.assertIsNone(result)
|
|
241
|
+
|
|
242
|
+
result = _normalize_model(" ")
|
|
243
|
+
self.assertIsNone(result)
|
|
244
|
+
|
|
245
|
+
def test_normalize_model_valid_names(self):
|
|
246
|
+
"""Test that _normalize_model accepts valid model names."""
|
|
247
|
+
valid_models = [
|
|
248
|
+
"sonnet",
|
|
249
|
+
"opus",
|
|
250
|
+
"haiku",
|
|
251
|
+
"gemini-3-pro-preview",
|
|
252
|
+
"gemini-3-auto",
|
|
253
|
+
"composer-1",
|
|
254
|
+
"model_name",
|
|
255
|
+
"model.name",
|
|
256
|
+
"model_name_123",
|
|
257
|
+
"a",
|
|
258
|
+
"123",
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
for model in valid_models:
|
|
262
|
+
with self.subTest(model=model):
|
|
263
|
+
result = _normalize_model(model)
|
|
264
|
+
self.assertEqual(result, model.strip())
|
|
265
|
+
|
|
266
|
+
# Test with whitespace
|
|
267
|
+
result = _normalize_model(f" {model} ")
|
|
268
|
+
self.assertEqual(result, model)
|
|
269
|
+
|
|
270
|
+
def test_normalize_model_invalid_names_raises_error(self):
|
|
271
|
+
"""Test that _normalize_model rejects invalid model names."""
|
|
272
|
+
invalid_models = [
|
|
273
|
+
"model with spaces",
|
|
274
|
+
"model@invalid",
|
|
275
|
+
"model#invalid",
|
|
276
|
+
"model$invalid",
|
|
277
|
+
"model%invalid",
|
|
278
|
+
"model&invalid",
|
|
279
|
+
"model*invalid",
|
|
280
|
+
"model+invalid",
|
|
281
|
+
"model=invalid",
|
|
282
|
+
"model[invalid",
|
|
283
|
+
"model]invalid",
|
|
284
|
+
"model{invalid",
|
|
285
|
+
"model}invalid",
|
|
286
|
+
"model|invalid",
|
|
287
|
+
"model\\invalid",
|
|
288
|
+
"model/invalid",
|
|
289
|
+
"model<invalid",
|
|
290
|
+
"model>invalid",
|
|
291
|
+
"model,invalid",
|
|
292
|
+
"model;invalid",
|
|
293
|
+
"model:invalid",
|
|
294
|
+
"model'invalid",
|
|
295
|
+
'model"invalid',
|
|
296
|
+
"model`invalid",
|
|
297
|
+
"model~invalid",
|
|
298
|
+
"model!invalid",
|
|
299
|
+
"model?invalid",
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
for model in invalid_models:
|
|
303
|
+
with self.subTest(model=model):
|
|
304
|
+
with self.assertRaises(argparse.ArgumentTypeError):
|
|
305
|
+
_normalize_model(model)
|
|
306
|
+
|
|
307
|
+
def test_normalize_model_strips_whitespace(self):
|
|
308
|
+
"""Test that _normalize_model strips whitespace from valid names."""
|
|
309
|
+
result = _normalize_model(" sonnet ")
|
|
310
|
+
self.assertEqual(result, "sonnet")
|
|
311
|
+
|
|
312
|
+
result = _normalize_model("\tgemini-3-auto\n")
|
|
313
|
+
self.assertEqual(result, "gemini-3-auto")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == '__main__':
|
|
317
|
+
unittest.main()
|