jleechanorg-pr-automation 0.1.0__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.

Potentially problematic release.


This version of jleechanorg-pr-automation might be problematic. Click here for more details.

@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ RED Phase: Matrix-driven TDD tests for PR filtering and actionable counting
4
+
5
+ Test Matrix Coverage:
6
+ - PR Status × Commit Changes × Processing History → Action + Count
7
+ - Batch Processing Logic with Skip Exclusion
8
+ - Eligible PR Detection and Filtering
9
+ """
10
+
11
+ import os
12
+ import unittest
13
+ import tempfile
14
+ import subprocess
15
+ from datetime import datetime, timedelta
16
+ from unittest.mock import Mock, patch, MagicMock
17
+
18
+ from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
19
+
20
+
21
+ class TestPRFilteringMatrix(unittest.TestCase):
22
+ """Matrix testing for PR filtering and actionable counting logic"""
23
+
24
+ def setUp(self):
25
+ """Set up test environment"""
26
+ self.temp_dir = tempfile.mkdtemp()
27
+ self.monitor = JleechanorgPRMonitor()
28
+ self.monitor.history_storage_path = self.temp_dir
29
+
30
+ def tearDown(self):
31
+ """Clean up test files"""
32
+ import shutil
33
+ shutil.rmtree(self.temp_dir)
34
+
35
+ # Matrix 1: PR Status × Commit Changes × Processing History
36
+ def test_matrix_open_pr_new_commit_never_processed_should_be_actionable(self):
37
+ """RED: Open PR with new commit, never processed → Should be actionable"""
38
+ pr_data = {
39
+ 'number': 1001,
40
+ 'title': 'Test PR',
41
+ 'state': 'open',
42
+ 'isDraft': False,
43
+ 'headRefName': 'feature-branch',
44
+ 'repository': 'test-repo',
45
+ 'repositoryFullName': 'org/test-repo',
46
+ 'headRefOid': 'abc123new'
47
+ }
48
+
49
+ # RED: This will fail - no is_pr_actionable method exists
50
+ result = self.monitor.is_pr_actionable(pr_data)
51
+ self.assertTrue(result)
52
+
53
+ def test_matrix_open_pr_same_commit_already_processed_should_not_be_actionable(self):
54
+ """RED: Open PR with same commit, already processed → Should not be actionable"""
55
+ pr_data = {
56
+ 'number': 1001,
57
+ 'title': 'Test PR',
58
+ 'state': 'open',
59
+ 'isDraft': False,
60
+ 'headRefName': 'feature-branch',
61
+ 'repository': 'test-repo',
62
+ 'repositoryFullName': 'org/test-repo',
63
+ 'headRefOid': 'abc123same'
64
+ }
65
+
66
+ # Simulate previous processing
67
+ self.monitor._record_pr_processing('test-repo', 'feature-branch', 1001, 'abc123same')
68
+
69
+ # RED: This will fail - no is_pr_actionable method exists
70
+ result = self.monitor.is_pr_actionable(pr_data)
71
+ self.assertFalse(result)
72
+
73
+ def test_matrix_open_pr_new_commit_old_commit_processed_should_be_actionable(self):
74
+ """RED: Open PR with new commit, old commit processed → Should be actionable"""
75
+ pr_data = {
76
+ 'number': 1001,
77
+ 'title': 'Test PR',
78
+ 'state': 'open',
79
+ 'isDraft': False,
80
+ 'headRefName': 'feature-branch',
81
+ 'repository': 'test-repo',
82
+ 'repositoryFullName': 'org/test-repo',
83
+ 'headRefOid': 'abc123new'
84
+ }
85
+
86
+ # Simulate processing of old commit
87
+ self.monitor._record_pr_processing('test-repo', 'feature-branch', 1001, 'abc123old')
88
+
89
+ # RED: This will fail - no is_pr_actionable method exists
90
+ result = self.monitor.is_pr_actionable(pr_data)
91
+ self.assertTrue(result)
92
+
93
+ def test_matrix_closed_pr_any_commit_should_not_be_actionable(self):
94
+ """RED: Closed PR with any commit → Should not be actionable"""
95
+ pr_data = {
96
+ 'number': 1001,
97
+ 'title': 'Test PR',
98
+ 'state': 'closed',
99
+ 'isDraft': False,
100
+ 'headRefName': 'feature-branch',
101
+ 'repository': 'test-repo',
102
+ 'repositoryFullName': 'org/test-repo',
103
+ 'headRefOid': 'abc123new'
104
+ }
105
+
106
+ # RED: This will fail - no is_pr_actionable method exists
107
+ result = self.monitor.is_pr_actionable(pr_data)
108
+ self.assertFalse(result)
109
+
110
+ def test_matrix_draft_pr_new_commit_never_processed_should_be_actionable(self):
111
+ """RED: Draft PR with new commit, never processed → Should be actionable"""
112
+ pr_data = {
113
+ 'number': 1001,
114
+ 'title': 'Test PR',
115
+ 'state': 'open',
116
+ 'isDraft': True,
117
+ 'headRefName': 'feature-branch',
118
+ 'repository': 'test-repo',
119
+ 'repositoryFullName': 'org/test-repo',
120
+ 'headRefOid': 'abc123new'
121
+ }
122
+
123
+ # RED: This will fail - no is_pr_actionable method exists
124
+ result = self.monitor.is_pr_actionable(pr_data)
125
+ self.assertTrue(result)
126
+
127
+ def test_matrix_open_pr_no_commits_should_not_be_actionable(self):
128
+ """RED: Open PR with no commits → Should not be actionable"""
129
+ pr_data = {
130
+ 'number': 1001,
131
+ 'title': 'Test PR',
132
+ 'state': 'open',
133
+ 'isDraft': False,
134
+ 'headRefName': 'feature-branch',
135
+ 'repository': 'test-repo',
136
+ 'repositoryFullName': 'org/test-repo',
137
+ 'headRefOid': None # No commits
138
+ }
139
+
140
+ # RED: This will fail - no is_pr_actionable method exists
141
+ result = self.monitor.is_pr_actionable(pr_data)
142
+ self.assertFalse(result)
143
+
144
+ # Matrix 2: Batch Processing Logic with Skip Exclusion
145
+ def test_matrix_batch_processing_15_eligible_target_10_should_process_10(self):
146
+ """RED: 15 eligible PRs, target 10 → Should process exactly 10"""
147
+ # Create 15 eligible PRs
148
+ eligible_prs = []
149
+ for i in range(15):
150
+ pr = {
151
+ 'number': 1000 + i,
152
+ 'title': f'Test PR {i}',
153
+ 'state': 'open',
154
+ 'isDraft': False,
155
+ 'headRefName': f'feature-branch-{i}',
156
+ 'repository': 'test-repo',
157
+ 'repositoryFullName': 'org/test-repo',
158
+ 'headRefOid': f'abc123{i:03d}'
159
+ }
160
+ eligible_prs.append(pr)
161
+
162
+ # RED: This will fail - no process_actionable_prs method exists
163
+ processed_count = self.monitor.process_actionable_prs(eligible_prs, target_count=10)
164
+ self.assertEqual(processed_count, 10)
165
+
166
+ def test_matrix_batch_processing_5_eligible_target_10_should_process_5(self):
167
+ """RED: 5 eligible PRs, target 10 → Should process all 5"""
168
+ # Create 5 eligible PRs
169
+ eligible_prs = []
170
+ for i in range(5):
171
+ pr = {
172
+ 'number': 1000 + i,
173
+ 'title': f'Test PR {i}',
174
+ 'state': 'open',
175
+ 'isDraft': False,
176
+ 'headRefName': f'feature-branch-{i}',
177
+ 'repository': 'test-repo',
178
+ 'repositoryFullName': 'org/test-repo',
179
+ 'headRefOid': f'abc123{i:03d}'
180
+ }
181
+ eligible_prs.append(pr)
182
+
183
+ # RED: This will fail - no process_actionable_prs method exists
184
+ processed_count = self.monitor.process_actionable_prs(eligible_prs, target_count=10)
185
+ self.assertEqual(processed_count, 5)
186
+
187
+ def test_matrix_batch_processing_0_eligible_target_10_should_process_0(self):
188
+ """RED: 0 eligible PRs, target 10 → Should process 0"""
189
+ eligible_prs = []
190
+
191
+ # RED: This will fail - no process_actionable_prs method exists
192
+ processed_count = self.monitor.process_actionable_prs(eligible_prs, target_count=10)
193
+ self.assertEqual(processed_count, 0)
194
+
195
+ def test_matrix_batch_processing_mixed_actionable_and_skipped_should_exclude_skipped_from_count(self):
196
+ """RED: Mixed actionable and skipped PRs → Should exclude skipped from count"""
197
+ # Create mixed PRs - some actionable, some already processed
198
+ all_prs = []
199
+
200
+ # 5 actionable PRs
201
+ for i in range(5):
202
+ pr = {
203
+ 'number': 1000 + i,
204
+ 'title': f'Actionable PR {i}',
205
+ 'state': 'open',
206
+ 'isDraft': False,
207
+ 'headRefName': f'feature-branch-{i}',
208
+ 'repository': 'test-repo',
209
+ 'repositoryFullName': 'org/test-repo',
210
+ 'headRefOid': f'abc123new{i:03d}'
211
+ }
212
+ all_prs.append(pr)
213
+
214
+ # 3 already processed PRs (should be skipped)
215
+ for i in range(3):
216
+ pr = {
217
+ 'number': 2000 + i,
218
+ 'title': f'Processed PR {i}',
219
+ 'state': 'open',
220
+ 'isDraft': False,
221
+ 'headRefName': f'processed-branch-{i}',
222
+ 'repository': 'test-repo',
223
+ 'repositoryFullName': 'org/test-repo',
224
+ 'headRefOid': f'abc123old{i:03d}'
225
+ }
226
+ # Pre-record as processed
227
+ self.monitor._record_pr_processing('test-repo', f'processed-branch-{i}', 2000 + i, f'abc123old{i:03d}')
228
+ all_prs.append(pr)
229
+
230
+ # RED: This will fail - no filter_and_process_prs method exists
231
+ processed_count = self.monitor.filter_and_process_prs(all_prs, target_actionable_count=10)
232
+ self.assertEqual(processed_count, 5) # Only actionable PRs counted
233
+
234
+ # Matrix 3: Eligible PR Detection
235
+ def test_matrix_filter_eligible_prs_from_mixed_list(self):
236
+ """RED: Filter eligible PRs from mixed list → Should return only actionable ones"""
237
+ mixed_prs = [
238
+ # Actionable: Open, new commit
239
+ {
240
+ 'number': 1001, 'state': 'open', 'isDraft': False,
241
+ 'headRefOid': 'new123', 'repository': 'repo1',
242
+ 'headRefName': 'branch1', 'repositoryFullName': 'org/repo1'
243
+ },
244
+ # Not actionable: Closed
245
+ {
246
+ 'number': 1002, 'state': 'closed', 'isDraft': False,
247
+ 'headRefOid': 'new456', 'repository': 'repo2',
248
+ 'headRefName': 'branch2', 'repositoryFullName': 'org/repo2'
249
+ },
250
+ # Not actionable: Already processed
251
+ {
252
+ 'number': 1003, 'state': 'open', 'isDraft': False,
253
+ 'headRefOid': 'old789', 'repository': 'repo3',
254
+ 'headRefName': 'branch3', 'repositoryFullName': 'org/repo3'
255
+ },
256
+ # Actionable: Draft but new commit
257
+ {
258
+ 'number': 1004, 'state': 'open', 'isDraft': True,
259
+ 'headRefOid': 'new999', 'repository': 'repo4',
260
+ 'headRefName': 'branch4', 'repositoryFullName': 'org/repo4'
261
+ }
262
+ ]
263
+
264
+ # Mark one as already processed
265
+ self.monitor._record_pr_processing('repo3', 'branch3', 1003, 'old789')
266
+
267
+ # RED: This will fail - no filter_eligible_prs method exists
268
+ eligible_prs = self.monitor.filter_eligible_prs(mixed_prs)
269
+
270
+ # Should return only the 2 actionable PRs
271
+ self.assertEqual(len(eligible_prs), 2)
272
+ actionable_numbers = [pr['number'] for pr in eligible_prs]
273
+ self.assertIn(1001, actionable_numbers)
274
+ self.assertIn(1004, actionable_numbers)
275
+ self.assertNotIn(1002, actionable_numbers) # Closed
276
+ self.assertNotIn(1003, actionable_numbers) # Already processed
277
+
278
+ def test_matrix_find_5_eligible_prs_from_live_data(self):
279
+ """RED: Find 5 eligible PRs from live GitHub data → Should return 5 actionable PRs"""
280
+ # Mock discover_open_prs to return test data instead of calling GitHub API
281
+ mock_prs = [
282
+ {"number": 1, "state": "open", "isDraft": False, "headRefOid": "abc123", "repository": "repo1", "headRefName": "feature1"},
283
+ {"number": 2, "state": "closed", "isDraft": False, "headRefOid": "def456", "repository": "repo2", "headRefName": "feature2"},
284
+ {"number": 3, "state": "open", "isDraft": False, "headRefOid": "ghi789", "repository": "repo3", "headRefName": "feature3"},
285
+ {"number": 4, "state": "open", "isDraft": True, "headRefOid": "jkl012", "repository": "repo4", "headRefName": "feature4"},
286
+ {"number": 5, "state": "open", "isDraft": False, "headRefOid": "mno345", "repository": "repo5", "headRefName": "feature5"},
287
+ {"number": 6, "state": "open", "isDraft": False, "headRefOid": "pqr678", "repository": "repo6", "headRefName": "feature6"},
288
+ {"number": 7, "state": "open", "isDraft": False, "headRefOid": "stu901", "repository": "repo7", "headRefName": "feature7"}
289
+ ]
290
+
291
+ with patch.object(self.monitor, 'discover_open_prs', return_value=mock_prs):
292
+ eligible_prs = self.monitor.find_eligible_prs(limit=5)
293
+ self.assertEqual(len(eligible_prs), 5)
294
+ # All returned PRs should be actionable
295
+ for pr in eligible_prs:
296
+ self.assertTrue(self.monitor.is_pr_actionable(pr))
297
+
298
+ # Matrix 5: Comment Posting Return Values (Bug Fix Tests)
299
+ def test_comment_posting_returns_posted_on_success(self):
300
+ """GREEN: Comment posting should return 'posted' when successful"""
301
+ with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
302
+ patch.object(self.monitor, '_should_skip_pr') as mock_skip, \
303
+ patch.object(self.monitor, '_has_codex_comment_for_commit') as mock_has_comment, \
304
+ patch.object(self.monitor, '_build_codex_comment_body_simple') as mock_build_body, \
305
+ patch.object(self.monitor, '_record_processed_pr') as mock_record, \
306
+ patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout') as mock_subprocess:
307
+
308
+ # Setup: PR not skipped, no existing comment, successful command
309
+ mock_state.return_value = ('sha123', [])
310
+ mock_skip.return_value = False
311
+ mock_has_comment.return_value = False
312
+ mock_build_body.return_value = 'Test comment body'
313
+ mock_subprocess.return_value = Mock(returncode=0, stdout='success', stderr='')
314
+
315
+ pr_data = {
316
+ 'repositoryFullName': 'org/repo',
317
+ 'headRefName': 'feature'
318
+ }
319
+
320
+ result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
321
+ self.assertEqual(result, 'posted')
322
+ mock_record.assert_called_once()
323
+
324
+ def test_comment_posting_returns_skipped_when_already_processed(self):
325
+ """GREEN: Comment posting should return 'skipped' when PR already processed"""
326
+ with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
327
+ patch.object(self.monitor, '_should_skip_pr') as mock_skip:
328
+
329
+ # Setup: PR should be skipped
330
+ mock_state.return_value = ('sha123', [])
331
+ mock_skip.return_value = True
332
+
333
+ pr_data = {
334
+ 'repositoryFullName': 'org/repo',
335
+ 'headRefName': 'feature'
336
+ }
337
+
338
+ result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
339
+ self.assertEqual(result, 'skipped')
340
+
341
+ def test_comment_posting_returns_skipped_when_comment_exists(self):
342
+ """GREEN: Comment posting should return 'skipped' when comment already exists for commit"""
343
+ with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
344
+ patch.object(self.monitor, '_should_skip_pr') as mock_skip, \
345
+ patch.object(self.monitor, '_has_codex_comment_for_commit') as mock_has_comment:
346
+
347
+ # Setup: PR not skipped but has existing comment
348
+ mock_state.return_value = ('sha123', [])
349
+ mock_skip.return_value = False
350
+ mock_has_comment.return_value = True
351
+
352
+ pr_data = {
353
+ 'repositoryFullName': 'org/repo',
354
+ 'headRefName': 'feature'
355
+ }
356
+
357
+ result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
358
+ self.assertEqual(result, 'skipped')
359
+
360
+ def test_comment_posting_returns_failed_on_subprocess_error(self):
361
+ """GREEN: Comment posting should return 'failed' when subprocess fails"""
362
+ with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
363
+ patch.object(self.monitor, '_should_skip_pr') as mock_skip, \
364
+ patch.object(self.monitor, '_has_codex_comment_for_commit') as mock_has_comment, \
365
+ patch.object(self.monitor, '_build_codex_comment_body_simple') as mock_build_body, \
366
+ patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout') as mock_subprocess:
367
+
368
+ # Setup: PR not skipped, no existing comment, but command fails
369
+ mock_state.return_value = ('sha123', [])
370
+ mock_skip.return_value = False
371
+ mock_has_comment.return_value = False
372
+ mock_build_body.return_value = 'Test comment body'
373
+ mock_subprocess.side_effect = Exception('Command failed')
374
+
375
+ pr_data = {
376
+ 'repositoryFullName': 'org/repo',
377
+ 'headRefName': 'feature'
378
+ }
379
+
380
+ result = self.monitor.post_codex_instruction_simple('org/repo', 123, pr_data)
381
+ self.assertEqual(result, 'failed')
382
+
383
+ def test_comment_posting_skips_when_head_commit_from_codex(self):
384
+ """GREEN: post_codex_instruction_simple should skip when head commit is Codex-attributed"""
385
+ with patch.object(self.monitor, '_get_pr_comment_state') as mock_state, \
386
+ patch.object(self.monitor, '_get_head_commit_details') as mock_head_details, \
387
+ patch.object(self.monitor, '_is_head_commit_from_codex') as mock_is_codex, \
388
+ patch.object(self.monitor, '_should_skip_pr') as mock_should_skip, \
389
+ patch.object(self.monitor, '_has_codex_comment_for_commit') as mock_has_comment, \
390
+ patch.object(self.monitor, '_record_processed_pr') as mock_record_processed, \
391
+ patch.object(self.monitor, '_build_codex_comment_body_simple') as mock_build_body, \
392
+ patch('jleechanorg_pr_automation.automation_utils.AutomationUtils.execute_subprocess_with_timeout') as mock_subprocess:
393
+
394
+ mock_state.return_value = ('sha123', [])
395
+ mock_head_details.return_value = {'sha': 'sha123'}
396
+ mock_is_codex.return_value = True
397
+
398
+ pr_data = {
399
+ 'repositoryFullName': 'org/repo',
400
+ 'headRefName': 'feature',
401
+ }
402
+
403
+ result = self.monitor.post_codex_instruction_simple('org/repo', 456, pr_data)
404
+
405
+ self.assertEqual(result, 'skipped')
406
+ mock_is_codex.assert_called_once_with({'sha': 'sha123'})
407
+ mock_should_skip.assert_not_called()
408
+ mock_has_comment.assert_not_called()
409
+ mock_build_body.assert_not_called()
410
+ mock_subprocess.assert_not_called()
411
+ mock_record_processed.assert_not_called()
412
+
413
+ def test_process_pr_comment_only_returns_true_for_posted(self):
414
+ """GREEN: _process_pr_comment should only return True when comment actually posted"""
415
+ with patch.object(self.monitor, 'post_codex_instruction_simple') as mock_post:
416
+
417
+ pr_data = {'repositoryFullName': 'org/repo'}
418
+
419
+ # Test: Returns True only for 'posted'
420
+ mock_post.return_value = 'posted'
421
+ self.assertTrue(self.monitor._process_pr_comment('repo', 123, pr_data))
422
+
423
+ # Test: Returns False for 'skipped'
424
+ mock_post.return_value = 'skipped'
425
+ self.assertFalse(self.monitor._process_pr_comment('repo', 123, pr_data))
426
+
427
+ # Test: Returns False for 'failed'
428
+ mock_post.return_value = 'failed'
429
+ self.assertFalse(self.monitor._process_pr_comment('repo', 123, pr_data))
430
+
431
+ def test_comment_template_contains_all_ai_assistants(self):
432
+ """GREEN: Comment template should mention all 4 AI assistants"""
433
+ pr_data = {
434
+ 'title': 'Test PR',
435
+ 'author': {'login': 'testuser'},
436
+ 'headRefName': 'test-branch'
437
+ }
438
+
439
+ comment_body = self.monitor._build_codex_comment_body_simple(
440
+ 'test/repo', 123, pr_data, 'abc12345'
441
+ )
442
+
443
+ # Verify all 4 AI assistant mentions are present
444
+ self.assertIn('@codex', comment_body, "Comment should mention @codex")
445
+ self.assertIn('@coderabbitai', comment_body, "Comment should mention @coderabbitai")
446
+ self.assertIn('@copilot', comment_body, "Comment should mention @copilot")
447
+ self.assertIn('@cursor', comment_body, "Comment should mention @cursor")
448
+
449
+ # Verify they appear at the beginning of the comment
450
+ first_line = comment_body.split('\n')[0]
451
+ self.assertIn('@codex', first_line, "@codex should be in first line")
452
+ self.assertIn('@coderabbitai', first_line, "@coderabbitai should be in first line")
453
+ self.assertIn('@copilot', first_line, "@copilot should be in first line")
454
+ self.assertIn('@cursor', first_line, "@cursor should be in first line")
455
+
456
+ # Verify automation marker instructions are documented
457
+ self.assertIn(
458
+ self.monitor.CODEX_COMMIT_MESSAGE_MARKER,
459
+ comment_body,
460
+ "Comment should instruct Codex to include the commit message marker",
461
+ )
462
+ self.assertIn(
463
+ "<!-- codex-automation-commit:",
464
+ comment_body,
465
+ "Comment should remind Codex about the hidden commit marker",
466
+ )
467
+
468
+
469
+ if __name__ == '__main__':
470
+ # RED Phase: Run tests to confirm they FAIL
471
+ print("🔴 RED Phase: Running failing tests for PR filtering matrix")
472
+ print("Expected: ALL TESTS SHOULD FAIL (no implementation exists)")
473
+ unittest.main(verbosity=2)
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test PR targeting functionality for jleechanorg_pr_monitor - Codex Strategy Tests Only
4
+ """
5
+
6
+ import unittest
7
+
8
+ from jleechanorg_pr_automation.jleechanorg_pr_monitor import JleechanorgPRMonitor
9
+
10
+
11
+ class TestPRTargeting(unittest.TestCase):
12
+ """Test PR targeting functionality - Codex Strategy Only"""
13
+
14
+ def test_extract_commit_marker(self):
15
+ """Commit markers can be parsed from Codex comments"""
16
+ monitor = JleechanorgPRMonitor()
17
+ test_comment = f"@codex @coderabbitai @copilot @cursor [AI automation] Test comment\n\n{monitor.CODEX_COMMIT_MARKER_PREFIX}abc123{monitor.CODEX_COMMIT_MARKER_SUFFIX}"
18
+ marker = monitor._extract_commit_marker(test_comment)
19
+ self.assertEqual(marker, "abc123")
20
+
21
+ def test_detect_pending_codex_commit(self):
22
+ """Codex bot summary comments referencing head commit trigger pending detection."""
23
+ monitor = JleechanorgPRMonitor()
24
+ head_sha = "abcdef1234567890"
25
+ comments = [
26
+ {
27
+ "body": "**Summary**\nlink https://github.com/org/repo/blob/abcdef1234567890/path/file.py\n",
28
+ "author": {"login": "chatgpt-codex-connector[bot]"},
29
+ }
30
+ ]
31
+
32
+ self.assertTrue(monitor._has_pending_codex_commit(comments, head_sha))
33
+
34
+ def test_pending_codex_commit_detects_short_sha_references(self):
35
+ """Cursor Bugbot short SHA references should still count as pending commits."""
36
+ monitor = JleechanorgPRMonitor()
37
+ full_head_sha = "c279655d00dfcab5ac1a2fd9b0f6205ce5cbba12"
38
+ comments = [
39
+ {
40
+ "body": "Written by Cursor Bugbot for commit c279655. This will update automatically on new commits.",
41
+ "author": {"login": "chatgpt-codex-connector[bot]"},
42
+ }
43
+ ]
44
+
45
+ self.assertTrue(monitor._has_pending_codex_commit(comments, full_head_sha))
46
+
47
+ def test_pending_codex_commit_ignores_short_head_sha(self):
48
+ """Short head SHAs should not match longer Codex summary hashes."""
49
+ monitor = JleechanorgPRMonitor()
50
+ short_head_sha = "c279655"
51
+ comments = [
52
+ {
53
+ "body": "Written by Cursor Bugbot for commit c279655d00dfcab5ac1a2fd9b0f6205ce5cbba12.",
54
+ "author": {"login": "chatgpt-codex-connector[bot]"},
55
+ }
56
+ ]
57
+
58
+ self.assertFalse(monitor._has_pending_codex_commit(comments, short_head_sha))
59
+
60
+ def test_pending_codex_commit_requires_codex_author(self):
61
+ """Pending detection ignores non-Codex authors even if commit appears in comment."""
62
+ monitor = JleechanorgPRMonitor()
63
+ head_sha = "abcdef1234567890"
64
+ comments = [
65
+ {
66
+ "body": "Please review commit https://github.com/org/repo/commit/abcdef1234567890",
67
+ "author": {"login": "reviewer"},
68
+ }
69
+ ]
70
+
71
+ self.assertFalse(monitor._has_pending_codex_commit(comments, head_sha))
72
+
73
+ def test_codex_comment_includes_detailed_execution_flow(self):
74
+ """Automation comment should summarize the enforced execution flow with numbered steps."""
75
+ monitor = JleechanorgPRMonitor()
76
+ pr_data = {
77
+ "title": "Improve automation summary",
78
+ "author": {"login": "developer"},
79
+ "headRefName": "feature/automation-flow",
80
+ }
81
+
82
+ comment_body = monitor._build_codex_comment_body_simple(
83
+ "jleechanorg/worldarchitect.ai",
84
+ 42,
85
+ pr_data,
86
+ "abcdef1234567890",
87
+ )
88
+
89
+ self.assertIn("**Summary (Execution Flow):**", comment_body)
90
+ self.assertIn("1. Review every outstanding PR comment", comment_body)
91
+ self.assertIn("5. Perform a final self-review", comment_body)
92
+
93
+
94
+ if __name__ == '__main__':
95
+ unittest.main()