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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. jleechanorg_pr_automation/STORAGE_STATE_TESTING_PROTOCOL.md +326 -0
  2. jleechanorg_pr_automation/__init__.py +64 -9
  3. jleechanorg_pr_automation/automation_safety_manager.py +306 -95
  4. jleechanorg_pr_automation/automation_safety_wrapper.py +13 -19
  5. jleechanorg_pr_automation/automation_utils.py +87 -65
  6. jleechanorg_pr_automation/check_codex_comment.py +7 -1
  7. jleechanorg_pr_automation/codex_branch_updater.py +21 -9
  8. jleechanorg_pr_automation/codex_config.py +70 -3
  9. jleechanorg_pr_automation/jleechanorg_pr_monitor.py +1954 -234
  10. jleechanorg_pr_automation/logging_utils.py +86 -0
  11. jleechanorg_pr_automation/openai_automation/__init__.py +3 -0
  12. jleechanorg_pr_automation/openai_automation/codex_github_mentions.py +1111 -0
  13. jleechanorg_pr_automation/openai_automation/debug_page_content.py +88 -0
  14. jleechanorg_pr_automation/openai_automation/oracle_cli.py +364 -0
  15. jleechanorg_pr_automation/openai_automation/test_auth_restoration.py +244 -0
  16. jleechanorg_pr_automation/openai_automation/test_codex_comprehensive.py +355 -0
  17. jleechanorg_pr_automation/openai_automation/test_codex_integration.py +254 -0
  18. jleechanorg_pr_automation/orchestrated_pr_runner.py +516 -0
  19. jleechanorg_pr_automation/tests/__init__.py +0 -0
  20. jleechanorg_pr_automation/tests/test_actionable_counting_matrix.py +84 -86
  21. jleechanorg_pr_automation/tests/test_attempt_limit_logic.py +124 -0
  22. jleechanorg_pr_automation/tests/test_automation_marker_functions.py +175 -0
  23. jleechanorg_pr_automation/tests/test_automation_over_running_reproduction.py +9 -11
  24. jleechanorg_pr_automation/tests/test_automation_safety_limits.py +91 -79
  25. jleechanorg_pr_automation/tests/test_automation_safety_manager_comprehensive.py +53 -53
  26. jleechanorg_pr_automation/tests/test_codex_actor_matching.py +1 -1
  27. jleechanorg_pr_automation/tests/test_fixpr_prompt.py +54 -0
  28. jleechanorg_pr_automation/tests/test_fixpr_return_value.py +140 -0
  29. jleechanorg_pr_automation/tests/test_graphql_error_handling.py +26 -26
  30. jleechanorg_pr_automation/tests/test_model_parameter.py +317 -0
  31. jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +697 -0
  32. jleechanorg_pr_automation/tests/test_packaging_integration.py +127 -0
  33. jleechanorg_pr_automation/tests/test_pr_filtering_matrix.py +246 -193
  34. jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +354 -0
  35. jleechanorg_pr_automation/tests/test_pr_targeting.py +102 -7
  36. jleechanorg_pr_automation/tests/test_version_consistency.py +51 -0
  37. jleechanorg_pr_automation/tests/test_workflow_specific_limits.py +202 -0
  38. jleechanorg_pr_automation/tests/test_workspace_dispatch_missing_dir.py +119 -0
  39. jleechanorg_pr_automation/utils.py +81 -56
  40. jleechanorg_pr_automation-0.2.45.dist-info/METADATA +864 -0
  41. jleechanorg_pr_automation-0.2.45.dist-info/RECORD +45 -0
  42. jleechanorg_pr_automation-0.1.1.dist-info/METADATA +0 -222
  43. jleechanorg_pr_automation-0.1.1.dist-info/RECORD +0 -23
  44. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/WHEEL +0 -0
  45. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/entry_points.txt +0 -0
  46. {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/top_level.txt +0 -0
@@ -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__ == '__main__':
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