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,354 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from _pytest.capture import CaptureFixture
|
|
4
|
+
from _pytest.monkeypatch import MonkeyPatch
|
|
5
|
+
from automation.jleechanorg_pr_automation import jleechanorg_pr_monitor as mon
|
|
6
|
+
|
|
7
|
+
FAILED_PR_NUMBER = 2
|
|
8
|
+
EXPECTED_ACTIONABLE_COUNT = 2
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def codex_marker(monitor: mon.JleechanorgPRMonitor, token: str) -> str:
|
|
12
|
+
return f"{monitor.CODEX_COMMIT_MARKER_PREFIX}{token}{monitor.CODEX_COMMIT_MARKER_SUFFIX}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_list_actionable_prs_conflicts_and_failing(monkeypatch: MonkeyPatch, capsys: CaptureFixture[str]) -> None:
|
|
16
|
+
monitor = mon.JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
17
|
+
|
|
18
|
+
sample_prs = [
|
|
19
|
+
{"repository": "repo/a", "number": 1, "title": "conflict", "mergeable": "CONFLICTING"},
|
|
20
|
+
{"repository": "repo/b", "number": 2, "title": "failing", "mergeable": "MERGEABLE"},
|
|
21
|
+
{"repository": "repo/c", "number": 3, "title": "passing", "mergeable": "MERGEABLE"},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
monkeypatch.setattr(monitor, "discover_open_prs", lambda: sample_prs)
|
|
25
|
+
|
|
26
|
+
def fake_has_failing_checks(repo: str, pr_number: int) -> bool: # noqa: ARG001
|
|
27
|
+
return pr_number == FAILED_PR_NUMBER
|
|
28
|
+
|
|
29
|
+
monkeypatch.setattr(mon, "has_failing_checks", fake_has_failing_checks)
|
|
30
|
+
|
|
31
|
+
actionable = monitor.list_actionable_prs(max_prs=10)
|
|
32
|
+
|
|
33
|
+
assert len(actionable) == EXPECTED_ACTIONABLE_COUNT
|
|
34
|
+
assert {pr["number"] for pr in actionable} == {1, FAILED_PR_NUMBER}
|
|
35
|
+
|
|
36
|
+
captured = capsys.readouterr().out
|
|
37
|
+
assert "Eligible for fixpr: 2" in captured
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestBotCommentDetection(unittest.TestCase):
|
|
41
|
+
"""Validate detection of new GitHub bot comments since last Codex automation comment."""
|
|
42
|
+
|
|
43
|
+
def setUp(self) -> None:
|
|
44
|
+
self.monitor = mon.JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
45
|
+
|
|
46
|
+
def test_identifies_new_github_actions_bot_comment(self) -> None:
|
|
47
|
+
"""Should detect new comment from github-actions[bot] after Codex comment."""
|
|
48
|
+
comments = [
|
|
49
|
+
{
|
|
50
|
+
"author": {"login": "jleechan"},
|
|
51
|
+
"body": f"@codex fix this {codex_marker(self.monitor, 'abc123')}",
|
|
52
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"author": {"login": "github-actions[bot]"},
|
|
56
|
+
"body": "CI failed: test_something assertion error",
|
|
57
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
assert self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
62
|
+
|
|
63
|
+
def test_identifies_new_dependabot_comment(self) -> None:
|
|
64
|
+
"""Should detect new comment from dependabot[bot] after Codex comment."""
|
|
65
|
+
comments = [
|
|
66
|
+
{
|
|
67
|
+
"author": {"login": "jleechan"},
|
|
68
|
+
"body": f"Fix issue {codex_marker(self.monitor, 'def456')}",
|
|
69
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"author": {"login": "dependabot[bot]"},
|
|
73
|
+
"body": "Security vulnerability detected",
|
|
74
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
assert self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
79
|
+
|
|
80
|
+
def test_no_detection_when_bot_comment_before_codex(self) -> None:
|
|
81
|
+
"""Should NOT detect bot comments that came BEFORE Codex comment."""
|
|
82
|
+
comments = [
|
|
83
|
+
{
|
|
84
|
+
"author": {"login": "github-actions[bot]"},
|
|
85
|
+
"body": "CI failed",
|
|
86
|
+
"createdAt": "2024-01-01T09:00:00Z",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"author": {"login": "jleechan"},
|
|
90
|
+
"body": f"@codex fix {codex_marker(self.monitor, 'abc123')}",
|
|
91
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
assert not self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
96
|
+
|
|
97
|
+
def test_identifies_bot_comment_without_prior_codex_comment(self) -> None:
|
|
98
|
+
"""Should treat any bot comment as new when no Codex automation comment exists."""
|
|
99
|
+
comments = [
|
|
100
|
+
{
|
|
101
|
+
"author": {"login": "github-actions[bot]"},
|
|
102
|
+
"body": "CI failed",
|
|
103
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"author": {"login": "jleechan"},
|
|
107
|
+
"body": "Regular comment without marker",
|
|
108
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
assert self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
113
|
+
|
|
114
|
+
def test_excludes_codex_bot_comments(self) -> None:
|
|
115
|
+
"""Should NOT count codex[bot] as a new bot comment to process."""
|
|
116
|
+
comments = [
|
|
117
|
+
{
|
|
118
|
+
"author": {"login": "jleechan"},
|
|
119
|
+
"body": f"@codex fix {codex_marker(self.monitor, 'abc123')}",
|
|
120
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"author": {"login": "codex[bot]"},
|
|
124
|
+
"body": "Codex summary: fixed the issue",
|
|
125
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
126
|
+
},
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
assert not self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
130
|
+
|
|
131
|
+
def test_identifies_coderabbitai_bot_comments(self) -> None:
|
|
132
|
+
"""Should count coderabbitai[bot] as a new bot comment to process."""
|
|
133
|
+
comments = [
|
|
134
|
+
{
|
|
135
|
+
"author": {"login": "jleechan"},
|
|
136
|
+
"body": f"@codex fix {codex_marker(self.monitor, 'abc123')}",
|
|
137
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"author": {"login": "coderabbitai[bot]"},
|
|
141
|
+
"body": "Code review completed",
|
|
142
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
assert self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
147
|
+
|
|
148
|
+
def test_excludes_copilot_bot_comments(self) -> None:
|
|
149
|
+
"""Should NOT count copilot[bot] as a new bot comment to process."""
|
|
150
|
+
comments = [
|
|
151
|
+
{
|
|
152
|
+
"author": {"login": "jleechan"},
|
|
153
|
+
"body": f"@codex fix {codex_marker(self.monitor, 'abc123')}",
|
|
154
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"author": {"login": "copilot[bot]"},
|
|
158
|
+
"body": "Copilot suggestion",
|
|
159
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
160
|
+
},
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
assert not self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
164
|
+
|
|
165
|
+
def test_ignores_human_comments_after_codex(self) -> None:
|
|
166
|
+
"""Human comments after Codex should NOT trigger new bot detection."""
|
|
167
|
+
comments = [
|
|
168
|
+
{
|
|
169
|
+
"author": {"login": "jleechan"},
|
|
170
|
+
"body": f"@codex fix {codex_marker(self.monitor, 'abc123')}",
|
|
171
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"author": {"login": "reviewer"},
|
|
175
|
+
"body": "LGTM",
|
|
176
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
assert not self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
181
|
+
|
|
182
|
+
def test_uses_latest_codex_comment_time(self) -> None:
|
|
183
|
+
"""Should use the timestamp of the MOST RECENT Codex comment."""
|
|
184
|
+
comments = [
|
|
185
|
+
{
|
|
186
|
+
"author": {"login": "jleechan"},
|
|
187
|
+
"body": f"Fix 1 {codex_marker(self.monitor, 'abc123')}",
|
|
188
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"author": {"login": "github-actions[bot]"},
|
|
192
|
+
"body": "CI failed",
|
|
193
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"author": {"login": "jleechan"},
|
|
197
|
+
"body": f"Fix 2 {codex_marker(self.monitor, 'def456')}",
|
|
198
|
+
"createdAt": "2024-01-01T12:00:00Z",
|
|
199
|
+
},
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
# Bot comment at 11:00 is BEFORE latest Codex comment at 12:00
|
|
203
|
+
assert not self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
204
|
+
|
|
205
|
+
def test_identifies_bot_comment_after_latest_codex(self) -> None:
|
|
206
|
+
"""Should detect bot comment that comes after the latest Codex comment."""
|
|
207
|
+
comments = [
|
|
208
|
+
{
|
|
209
|
+
"author": {"login": "jleechan"},
|
|
210
|
+
"body": f"Fix 1 {codex_marker(self.monitor, 'abc123')}",
|
|
211
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"author": {"login": "jleechan"},
|
|
215
|
+
"body": f"Fix 2 {codex_marker(self.monitor, 'def456')}",
|
|
216
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"author": {"login": "github-actions[bot]"},
|
|
220
|
+
"body": "CI still failing",
|
|
221
|
+
"createdAt": "2024-01-01T12:00:00Z",
|
|
222
|
+
},
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
assert self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
226
|
+
|
|
227
|
+
def test_handles_empty_comments_list(self) -> None:
|
|
228
|
+
"""Should handle empty comments list gracefully."""
|
|
229
|
+
assert not self.monitor._has_new_bot_comments_since_codex([]) # noqa: SLF001
|
|
230
|
+
|
|
231
|
+
def test_handles_missing_author(self) -> None:
|
|
232
|
+
"""Should handle comments with missing author field."""
|
|
233
|
+
comments = [
|
|
234
|
+
{
|
|
235
|
+
"author": {"login": "jleechan"},
|
|
236
|
+
"body": f"Fix {codex_marker(self.monitor, 'abc123')}",
|
|
237
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"body": "Comment with no author",
|
|
241
|
+
"createdAt": "2024-01-01T11:00:00Z",
|
|
242
|
+
},
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
# Should not crash and should return False (no valid bot comment)
|
|
246
|
+
assert not self.monitor._has_new_bot_comments_since_codex(comments) # noqa: SLF001
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TestIsGithubBotComment(unittest.TestCase):
|
|
250
|
+
"""Validate _is_github_bot_comment method."""
|
|
251
|
+
|
|
252
|
+
def setUp(self) -> None:
|
|
253
|
+
self.monitor = mon.JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
254
|
+
|
|
255
|
+
def test_identifies_github_actions_bot(self) -> None:
|
|
256
|
+
comment = {"author": {"login": "github-actions[bot]"}}
|
|
257
|
+
assert self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
258
|
+
|
|
259
|
+
def test_identifies_dependabot(self) -> None:
|
|
260
|
+
comment = {"author": {"login": "dependabot[bot]"}}
|
|
261
|
+
assert self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
262
|
+
|
|
263
|
+
def test_identifies_renovate_bot(self) -> None:
|
|
264
|
+
comment = {"author": {"login": "renovate[bot]"}}
|
|
265
|
+
assert self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
266
|
+
|
|
267
|
+
def test_identifies_coderabbitai_without_bot_suffix(self) -> None:
|
|
268
|
+
comment = {"author": {"login": "coderabbitai"}}
|
|
269
|
+
assert self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
270
|
+
|
|
271
|
+
def test_identifies_copilot_swe_agent_without_bot_suffix(self) -> None:
|
|
272
|
+
comment = {"author": {"login": "copilot-swe-agent"}}
|
|
273
|
+
assert self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
274
|
+
|
|
275
|
+
def test_identifies_github_actions_without_bot_suffix(self) -> None:
|
|
276
|
+
comment = {"author": {"login": "github-actions"}}
|
|
277
|
+
assert self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
278
|
+
|
|
279
|
+
def test_excludes_codex_bot(self) -> None:
|
|
280
|
+
comment = {"author": {"login": "codex[bot]"}}
|
|
281
|
+
assert not self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
282
|
+
|
|
283
|
+
def test_identifies_coderabbitai_bot_with_suffix(self) -> None:
|
|
284
|
+
comment = {"author": {"login": "coderabbitai[bot]"}}
|
|
285
|
+
assert self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
286
|
+
|
|
287
|
+
def test_excludes_copilot_bot(self) -> None:
|
|
288
|
+
comment = {"author": {"login": "copilot[bot]"}}
|
|
289
|
+
assert not self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
290
|
+
|
|
291
|
+
def test_excludes_cursor_bot(self) -> None:
|
|
292
|
+
comment = {"author": {"login": "cursor[bot]"}}
|
|
293
|
+
assert not self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
294
|
+
|
|
295
|
+
def test_excludes_human_user(self) -> None:
|
|
296
|
+
comment = {"author": {"login": "jleechan"}}
|
|
297
|
+
assert not self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
298
|
+
|
|
299
|
+
def test_handles_user_field_fallback(self) -> None:
|
|
300
|
+
comment = {"user": {"login": "github-actions[bot]"}}
|
|
301
|
+
assert self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
302
|
+
|
|
303
|
+
def test_handles_empty_author(self) -> None:
|
|
304
|
+
comment = {"author": {}}
|
|
305
|
+
assert not self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
306
|
+
|
|
307
|
+
def test_handles_missing_author(self) -> None:
|
|
308
|
+
comment = {"body": "no author"}
|
|
309
|
+
assert not self.monitor._is_github_bot_comment(comment) # noqa: SLF001
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestGetLastCodexAutomationCommentTime(unittest.TestCase):
|
|
313
|
+
"""Validate _get_last_codex_automation_comment_time method."""
|
|
314
|
+
|
|
315
|
+
def setUp(self) -> None:
|
|
316
|
+
self.monitor = mon.JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
317
|
+
|
|
318
|
+
def test_returns_latest_codex_comment_time(self) -> None:
|
|
319
|
+
comments = [
|
|
320
|
+
{
|
|
321
|
+
"body": f"First {self.monitor.CODEX_COMMIT_MARKER_PREFIX}abc{self.monitor.CODEX_COMMIT_MARKER_SUFFIX}",
|
|
322
|
+
"createdAt": "2024-01-01T10:00:00Z",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"body": f"Second {self.monitor.CODEX_COMMIT_MARKER_PREFIX}def{self.monitor.CODEX_COMMIT_MARKER_SUFFIX}",
|
|
326
|
+
"createdAt": "2024-01-01T12:00:00Z",
|
|
327
|
+
},
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
result = self.monitor._get_last_codex_automation_comment_time(comments) # noqa: SLF001
|
|
331
|
+
assert result == "2024-01-01T12:00:00Z"
|
|
332
|
+
|
|
333
|
+
def test_returns_none_when_no_codex_comments(self) -> None:
|
|
334
|
+
comments = [
|
|
335
|
+
{"body": "Regular comment", "createdAt": "2024-01-01T10:00:00Z"},
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
result = self.monitor._get_last_codex_automation_comment_time(comments) # noqa: SLF001
|
|
339
|
+
assert result is None
|
|
340
|
+
|
|
341
|
+
def test_returns_none_for_empty_list(self) -> None:
|
|
342
|
+
result = self.monitor._get_last_codex_automation_comment_time([]) # noqa: SLF001
|
|
343
|
+
assert result is None
|
|
344
|
+
|
|
345
|
+
def test_uses_updated_at_fallback(self) -> None:
|
|
346
|
+
comments = [
|
|
347
|
+
{
|
|
348
|
+
"body": f"Update {self.monitor.CODEX_COMMIT_MARKER_PREFIX}xyz{self.monitor.CODEX_COMMIT_MARKER_SUFFIX}",
|
|
349
|
+
"updatedAt": "2024-01-01T15:00:00Z",
|
|
350
|
+
},
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
result = self.monitor._get_last_codex_automation_comment_time(comments) # noqa: SLF001
|
|
354
|
+
assert result == "2024-01-01T15:00:00Z"
|
|
@@ -14,7 +14,7 @@ class TestPRTargeting(unittest.TestCase):
|
|
|
14
14
|
|
|
15
15
|
def test_extract_commit_marker(self):
|
|
16
16
|
"""Commit markers can be parsed from Codex comments"""
|
|
17
|
-
monitor = JleechanorgPRMonitor()
|
|
17
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
18
18
|
intro_line = build_comment_intro(
|
|
19
19
|
assistant_mentions=monitor.assistant_mentions
|
|
20
20
|
)
|
|
@@ -25,6 +25,18 @@ class TestPRTargeting(unittest.TestCase):
|
|
|
25
25
|
marker = monitor._extract_commit_marker(test_comment)
|
|
26
26
|
self.assertEqual(marker, "abc123")
|
|
27
27
|
|
|
28
|
+
def test_fix_comment_marker_detected_for_commit(self):
|
|
29
|
+
"""Fix-comment markers should be detected for commit gating."""
|
|
30
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
31
|
+
test_comment = (
|
|
32
|
+
"Queued\n"
|
|
33
|
+
f"{monitor.FIX_COMMENT_MARKER_PREFIX}abc123"
|
|
34
|
+
f"{monitor.FIX_COMMENT_MARKER_SUFFIX}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
marker = monitor._extract_fix_comment_marker(test_comment)
|
|
38
|
+
self.assertEqual(marker, "abc123")
|
|
39
|
+
|
|
28
40
|
def test_intro_prose_avoids_duplicate_mentions(self):
|
|
29
41
|
"""Review assistants should not retain '@' prefixes in prose text."""
|
|
30
42
|
|
|
@@ -43,7 +55,7 @@ class TestPRTargeting(unittest.TestCase):
|
|
|
43
55
|
|
|
44
56
|
def test_detect_pending_codex_commit(self):
|
|
45
57
|
"""Codex bot summary comments referencing head commit trigger pending detection."""
|
|
46
|
-
monitor = JleechanorgPRMonitor()
|
|
58
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
47
59
|
head_sha = "abcdef1234567890"
|
|
48
60
|
comments = [
|
|
49
61
|
{
|
|
@@ -56,7 +68,7 @@ class TestPRTargeting(unittest.TestCase):
|
|
|
56
68
|
|
|
57
69
|
def test_pending_codex_commit_detects_short_sha_references(self):
|
|
58
70
|
"""Cursor Bugbot short SHA references should still count as pending commits."""
|
|
59
|
-
monitor = JleechanorgPRMonitor()
|
|
71
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
60
72
|
full_head_sha = "c279655d00dfcab5ac1a2fd9b0f6205ce5cbba12"
|
|
61
73
|
comments = [
|
|
62
74
|
{
|
|
@@ -69,7 +81,7 @@ class TestPRTargeting(unittest.TestCase):
|
|
|
69
81
|
|
|
70
82
|
def test_pending_codex_commit_ignores_short_head_sha(self):
|
|
71
83
|
"""Short head SHAs should not match longer Codex summary hashes."""
|
|
72
|
-
monitor = JleechanorgPRMonitor()
|
|
84
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
73
85
|
short_head_sha = "c279655"
|
|
74
86
|
comments = [
|
|
75
87
|
{
|
|
@@ -82,7 +94,7 @@ class TestPRTargeting(unittest.TestCase):
|
|
|
82
94
|
|
|
83
95
|
def test_pending_codex_commit_requires_codex_author(self):
|
|
84
96
|
"""Pending detection ignores non-Codex authors even if commit appears in comment."""
|
|
85
|
-
monitor = JleechanorgPRMonitor()
|
|
97
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
86
98
|
head_sha = "abcdef1234567890"
|
|
87
99
|
comments = [
|
|
88
100
|
{
|
|
@@ -95,7 +107,7 @@ class TestPRTargeting(unittest.TestCase):
|
|
|
95
107
|
|
|
96
108
|
def test_codex_comment_includes_detailed_execution_flow(self):
|
|
97
109
|
"""Automation comment should summarize the enforced execution flow with numbered steps."""
|
|
98
|
-
monitor = JleechanorgPRMonitor()
|
|
110
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
99
111
|
pr_data = {
|
|
100
112
|
"title": "Improve automation summary",
|
|
101
113
|
"author": {"login": "developer"},
|
|
@@ -113,6 +125,89 @@ class TestPRTargeting(unittest.TestCase):
|
|
|
113
125
|
self.assertIn("1. Review every outstanding PR comment", comment_body)
|
|
114
126
|
self.assertIn("5. Perform a final self-review", comment_body)
|
|
115
127
|
|
|
128
|
+
def test_fix_comment_queued_body_excludes_marker(self):
|
|
129
|
+
"""Queued fix-comment notices should not include commit markers."""
|
|
130
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
131
|
+
pr_data = {
|
|
132
|
+
"title": "Queued Fix Comment",
|
|
133
|
+
"author": {"login": "developer"},
|
|
134
|
+
"headRefName": "feature/queued",
|
|
135
|
+
}
|
|
136
|
+
head_sha = "abc123def456"
|
|
137
|
+
|
|
138
|
+
comment_body = monitor._build_fix_comment_queued_body(
|
|
139
|
+
"org/repo",
|
|
140
|
+
42,
|
|
141
|
+
pr_data,
|
|
142
|
+
head_sha,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
self.assertNotIn(monitor.FIX_COMMENT_MARKER_PREFIX, comment_body)
|
|
146
|
+
# Queued notices should use the dedicated run marker (not the completion commit marker).
|
|
147
|
+
self.assertIn(monitor.FIX_COMMENT_RUN_MARKER_PREFIX, comment_body)
|
|
148
|
+
self.assertIn(head_sha, comment_body)
|
|
149
|
+
|
|
150
|
+
def test_fix_comment_review_body_includes_marker(self):
|
|
151
|
+
"""Review requests should include the fix-comment commit marker."""
|
|
152
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
153
|
+
pr_data = {
|
|
154
|
+
"title": "Review Fix Comment",
|
|
155
|
+
"author": {"login": "developer"},
|
|
156
|
+
"headRefName": "feature/review",
|
|
157
|
+
}
|
|
158
|
+
head_sha = "deadbeef1234"
|
|
159
|
+
|
|
160
|
+
comment_body = monitor._build_fix_comment_review_body(
|
|
161
|
+
"org/repo",
|
|
162
|
+
42,
|
|
163
|
+
pr_data,
|
|
164
|
+
head_sha,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self.assertIn(monitor.FIX_COMMENT_MARKER_PREFIX, comment_body)
|
|
168
|
+
self.assertIn(monitor.FIX_COMMENT_MARKER_SUFFIX, comment_body)
|
|
169
|
+
|
|
170
|
+
def test_fix_comment_prompt_requires_threaded_replies(self):
|
|
171
|
+
"""Fix-comment prompts should require threaded replies via the GitHub API."""
|
|
172
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
173
|
+
pr_data = {
|
|
174
|
+
"title": "Threaded Replies",
|
|
175
|
+
"author": {"login": "developer"},
|
|
176
|
+
"headRefName": "feature/threaded",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
prompt = monitor._build_fix_comment_prompt_body(
|
|
180
|
+
"org/repo",
|
|
181
|
+
42,
|
|
182
|
+
pr_data,
|
|
183
|
+
"abc123",
|
|
184
|
+
agent_cli="gemini",
|
|
185
|
+
).lower()
|
|
186
|
+
|
|
187
|
+
# Verify prompt includes threading guidance
|
|
188
|
+
self.assertIn("thread", prompt)
|
|
189
|
+
self.assertIn("gh api", prompt)
|
|
190
|
+
self.assertIn("review comments", prompt)
|
|
191
|
+
self.assertIn("issue comments", prompt)
|
|
192
|
+
# After fix for comment #2669657213, prompt clarifies:
|
|
193
|
+
# - Inline review comments use: /pulls/{pr_number}/comments/{comment_id}/replies
|
|
194
|
+
# - Issue comments don't support threading (top-level comments only)
|
|
195
|
+
self.assertIn("pulls/42/comments", prompt) # Updated to match actual PR number in prompt
|
|
196
|
+
self.assertIn("reply individually to each comment", prompt) # Issue comments clarification
|
|
197
|
+
|
|
198
|
+
def test_fix_comment_marker_ignores_queued_comment(self):
|
|
199
|
+
"""Queued notices with markers should not satisfy the fix-comment completion check."""
|
|
200
|
+
monitor = JleechanorgPRMonitor(automation_username="test-automation-user")
|
|
201
|
+
head_sha = "feedface1234"
|
|
202
|
+
comment_body = (
|
|
203
|
+
"[AI automation] Fix-comment run queued for this PR. "
|
|
204
|
+
"A review request will follow after updates are pushed.\n\n"
|
|
205
|
+
f"{monitor.FIX_COMMENT_MARKER_PREFIX}{head_sha}{monitor.FIX_COMMENT_MARKER_SUFFIX}"
|
|
206
|
+
)
|
|
207
|
+
comments = [{"body": comment_body}]
|
|
208
|
+
|
|
209
|
+
self.assertFalse(monitor._has_fix_comment_comment_for_commit(comments, head_sha))
|
|
210
|
+
|
|
116
211
|
|
|
117
|
-
if __name__ ==
|
|
212
|
+
if __name__ == "__main__":
|
|
118
213
|
unittest.main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import jleechanorg_pr_automation
|
|
7
|
+
|
|
8
|
+
_PROJECT_SECTION_RE = re.compile(r"^\s*\[project\]\s*$")
|
|
9
|
+
_SECTION_RE = re.compile(r"^\s*\[[^\]]+\]\s*$")
|
|
10
|
+
_VERSION_RE = re.compile(r'^\s*version\s*=\s*"([^"]+)"\s*$')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _version_from_pyproject(pyproject_path: Path) -> Optional[str]:
|
|
14
|
+
if not pyproject_path.exists():
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
in_project_section = False
|
|
18
|
+
for line in pyproject_path.read_text(encoding="utf-8").splitlines():
|
|
19
|
+
if _PROJECT_SECTION_RE.match(line):
|
|
20
|
+
in_project_section = True
|
|
21
|
+
continue
|
|
22
|
+
if in_project_section and _SECTION_RE.match(line):
|
|
23
|
+
in_project_section = False
|
|
24
|
+
continue
|
|
25
|
+
if not in_project_section:
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
match = _VERSION_RE.match(line)
|
|
29
|
+
if match:
|
|
30
|
+
version = match.group(1).strip()
|
|
31
|
+
return version or None
|
|
32
|
+
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_package_version_matches_pyproject():
|
|
37
|
+
"""
|
|
38
|
+
Keep package version declarations consistent.
|
|
39
|
+
|
|
40
|
+
Cursor/Copilot bots frequently flag mismatches between:
|
|
41
|
+
- automation/pyproject.toml [project].version
|
|
42
|
+
- automation/jleechanorg_pr_automation/__init__.py __version__
|
|
43
|
+
"""
|
|
44
|
+
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
45
|
+
if not pyproject_path.exists():
|
|
46
|
+
pytest.skip(f"pyproject not found at {pyproject_path}")
|
|
47
|
+
|
|
48
|
+
pyproject_version = _version_from_pyproject(pyproject_path)
|
|
49
|
+
if pyproject_version is None:
|
|
50
|
+
pytest.fail(f"Unable to parse [project].version from {pyproject_path}")
|
|
51
|
+
assert jleechanorg_pr_automation.__version__ == pyproject_version
|