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,355 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Comprehensive Matrix-Driven Tests for Codex GitHub Mentions Automation.
4
+
5
+ Test Matrices:
6
+ - Matrix 1: Limit Parameter Combinations (12 tests)
7
+ - Matrix 2: CDP Connection States (8 tests)
8
+ - Matrix 3: Task Finding Scenarios (10 tests)
9
+ - Matrix 4: Navigation & Interaction (8 tests)
10
+
11
+ Total: 38 systematic test cases
12
+
13
+ Run with:
14
+ pytest automation/openai_automation/test_codex_comprehensive.py -v
15
+ """
16
+
17
+ import asyncio
18
+ from unittest.mock import AsyncMock, Mock
19
+
20
+ import aiohttp
21
+ import pytest
22
+
23
+ from jleechanorg_pr_automation.openai_automation.codex_github_mentions import (
24
+ CodexGitHubMentionsAutomation,
25
+ )
26
+
27
+
28
+ # Helper to check if Chrome is running with CDP
29
+ async def chrome_is_running(port=9222):
30
+ """Check if Chrome is running with remote debugging."""
31
+ try:
32
+ async with aiohttp.ClientSession() as session:
33
+ async with session.get(
34
+ f"http://localhost:{port}/json/version", timeout=aiohttp.ClientTimeout(total=1)
35
+ ) as resp:
36
+ return resp.status == 200
37
+ except (aiohttp.ClientError, asyncio.TimeoutError, OSError):
38
+ return False
39
+
40
+
41
+ # Skip marker for tests requiring Chrome
42
+ requires_chrome = pytest.mark.skipif(
43
+ not asyncio.run(chrome_is_running()),
44
+ reason="Chrome with remote debugging not running on port 9222"
45
+ )
46
+
47
+
48
+ class TestLimitParameter:
49
+ """Matrix 1: Test limit parameter combinations."""
50
+
51
+ @pytest.mark.parametrize("limit,expected_behavior", [
52
+ (None, "GitHub Mention tasks only"),
53
+ (50, "Limit GitHub Mention tasks to 50"),
54
+ (10, "Limit GitHub Mention tasks to 10"),
55
+ (100, "Limit GitHub Mention tasks to 100"),
56
+ (0, "No tasks processed"),
57
+ (1, "Limit GitHub Mention tasks to 1"),
58
+ (5, "Limit GitHub Mention tasks to 5"),
59
+ ])
60
+ def test_limit_initialization(self, limit, expected_behavior):
61
+ """Test automation initialization with different limit values."""
62
+ automation = CodexGitHubMentionsAutomation(task_limit=limit)
63
+ assert automation.task_limit == limit
64
+ print(f"✅ Limit {limit}: {expected_behavior}")
65
+
66
+ @pytest.mark.asyncio
67
+ @pytest.mark.parametrize("limit,mock_task_count,expected_return", [
68
+ (50, 100, 50), # Limit to 50 when 100 available
69
+ (10, 5, 5), # Return all when fewer than limit
70
+ (None, 20, 20), # No limit, return all
71
+ (0, 50, 0), # Zero limit, return none
72
+ ])
73
+ async def test_limit_applied_to_task_finding(self, limit, mock_task_count, expected_return):
74
+ """Test that limit is correctly applied when finding tasks."""
75
+ automation = CodexGitHubMentionsAutomation(task_limit=limit)
76
+ automation.page = AsyncMock()
77
+
78
+ mock_locator = AsyncMock()
79
+ task_items = []
80
+ for i in range(mock_task_count):
81
+ item = Mock()
82
+ item.get_attribute = AsyncMock(return_value=f"/codex/{i}")
83
+ item.text_content = AsyncMock(return_value=f"Task {i}")
84
+ task_items.append(item)
85
+
86
+ mock_locator.count = AsyncMock(return_value=mock_task_count)
87
+ mock_locator.nth = Mock(side_effect=lambda idx: task_items[idx])
88
+ automation.page.locator = Mock(return_value=mock_locator)
89
+
90
+ tasks = await automation.find_github_mention_tasks()
91
+ assert len(tasks) == expected_return
92
+ print(f"✅ Limit {limit} with {mock_task_count} tasks returned {expected_return}")
93
+
94
+ @requires_chrome
95
+ @pytest.mark.asyncio
96
+ async def test_default_limit_50_with_real_chrome(self):
97
+ """Test default limit of 50 with real Chrome instance."""
98
+ automation = CodexGitHubMentionsAutomation()
99
+ assert automation.task_limit == 50
100
+
101
+ await automation.connect_to_existing_browser()
102
+ await automation.navigate_to_codex()
103
+
104
+ tasks = await automation.find_github_mention_tasks()
105
+ # Should use ALL tasks selector when limit is set
106
+ assert isinstance(tasks, list)
107
+ assert len(tasks) <= 50
108
+ print(f"✅ Default limit 50 found {len(tasks)} tasks")
109
+
110
+
111
+ class TestCDPConnectionStates:
112
+ """Matrix 2: Test CDP connection state handling."""
113
+
114
+ @pytest.mark.asyncio
115
+ async def test_connect_chrome_not_running(self):
116
+ """Test connection failure when Chrome is not running."""
117
+ automation = CodexGitHubMentionsAutomation(cdp_url="http://localhost:9999")
118
+
119
+ result = await automation.connect_to_existing_browser()
120
+ assert result is False
121
+ assert automation.browser is None
122
+ print("✅ Correctly handled Chrome not running")
123
+
124
+ @pytest.mark.asyncio
125
+ async def test_connect_wrong_port(self):
126
+ """Test connection failure on wrong port."""
127
+ automation = CodexGitHubMentionsAutomation(cdp_url="http://localhost:1234")
128
+
129
+ result = await automation.connect_to_existing_browser()
130
+ assert result is False
131
+ print("✅ Correctly handled wrong port")
132
+
133
+ @requires_chrome
134
+ @pytest.mark.asyncio
135
+ async def test_connect_success_on_9222(self):
136
+ """Test successful connection on default port 9222."""
137
+ automation = CodexGitHubMentionsAutomation()
138
+
139
+ result = await automation.connect_to_existing_browser()
140
+ assert result is True
141
+ assert automation.browser is not None
142
+ assert automation.page is not None
143
+ print("✅ Successfully connected to Chrome on port 9222")
144
+
145
+ @pytest.mark.asyncio
146
+ async def test_no_contexts_creates_new(self):
147
+ """Test that automation creates new context when none exist."""
148
+ automation = CodexGitHubMentionsAutomation()
149
+
150
+ # Mock browser with no contexts
151
+ mock_browser = AsyncMock()
152
+ mock_browser.contexts = []
153
+ mock_browser.new_context = AsyncMock()
154
+ mock_context = AsyncMock()
155
+ mock_context.pages = []
156
+ mock_context.new_page = AsyncMock(return_value=AsyncMock())
157
+ mock_browser.new_context.return_value = mock_context
158
+
159
+ automation.browser = mock_browser
160
+
161
+ # This would normally be in connect_to_existing_browser
162
+ if not automation.browser.contexts:
163
+ automation.context = await automation.browser.new_context()
164
+ automation.page = await automation.context.new_page()
165
+
166
+ assert automation.context is not None
167
+ assert automation.page is not None
168
+ print("✅ Created new context when none existed")
169
+
170
+
171
+ class TestTaskFinding:
172
+ """Matrix 3: Test task finding scenarios."""
173
+
174
+ @pytest.mark.asyncio
175
+ @pytest.mark.parametrize("task_count,limit,expected_found,behavior", [
176
+ (0, 50, 0, "Graceful empty"),
177
+ (5, 50, 5, "All found"),
178
+ (100, 50, 50, "Limited"),
179
+ (10, None, 10, "GitHub only filter"),
180
+ (25, 10, 10, "Limited to 10"),
181
+ (3, 100, 3, "All found (fewer than limit)"),
182
+ ])
183
+ async def test_task_finding_matrix(self, task_count, limit, expected_found, behavior):
184
+ """Test various task finding scenarios from matrix."""
185
+ automation = CodexGitHubMentionsAutomation(task_limit=limit)
186
+ automation.page = AsyncMock()
187
+
188
+ # Mock task locator
189
+ mock_locator = AsyncMock()
190
+ mock_tasks = []
191
+ for idx in range(task_count):
192
+ item = Mock()
193
+ item.get_attribute = AsyncMock(return_value=f"/codex/{idx}")
194
+ item.text_content = AsyncMock(return_value=f"Task {idx}")
195
+ mock_tasks.append(item)
196
+
197
+ mock_locator.count = AsyncMock(return_value=task_count)
198
+ mock_locator.nth = Mock(side_effect=lambda idx: mock_tasks[idx])
199
+ automation.page.locator = Mock(return_value=mock_locator)
200
+
201
+ tasks = await automation.find_github_mention_tasks()
202
+
203
+ assert len(tasks) == expected_found
204
+ print(f"✅ {behavior}: {task_count} tasks, limit={limit} → found {expected_found}")
205
+
206
+ @pytest.mark.asyncio
207
+ async def test_github_mention_selector_used_when_no_limit(self):
208
+ """Test that task selector is used (now always uses /codex/tasks/ to exclude navigation)."""
209
+ automation = CodexGitHubMentionsAutomation(task_limit=None)
210
+ automation.page = AsyncMock()
211
+
212
+ mock_locator = AsyncMock()
213
+ mock_locator.count = AsyncMock(return_value=0)
214
+ automation.page.locator = Mock(return_value=mock_locator)
215
+
216
+ await automation.find_github_mention_tasks()
217
+
218
+ # Verify correct selector was used - now uses /codex/tasks/ to exclude navigation links
219
+ automation.page.locator.assert_any_call('a[href*="/codex/tasks/"]')
220
+ print("✅ Correct selector used for None limit")
221
+
222
+ @pytest.mark.asyncio
223
+ async def test_all_tasks_selector_used_when_limit_set(self):
224
+ """Test that task selector is used (now always uses /codex/tasks/ to exclude navigation)."""
225
+ automation = CodexGitHubMentionsAutomation(task_limit=50, all_tasks=True)
226
+ automation.page = AsyncMock()
227
+
228
+ mock_locator = AsyncMock()
229
+ mock_locator.count = AsyncMock(return_value=0)
230
+ automation.page.locator = Mock(return_value=mock_locator)
231
+
232
+ await automation.find_github_mention_tasks()
233
+
234
+ # Verify correct selector was used - now uses /codex/tasks/ to exclude navigation links
235
+ automation.page.locator.assert_any_call('a[href*="/codex/tasks/"]')
236
+ print("✅ Correct selector used for limit=50")
237
+
238
+
239
+ class TestNavigationInteraction:
240
+ """Matrix 4: Test navigation and interaction scenarios."""
241
+
242
+ @pytest.mark.asyncio
243
+ async def test_navigate_to_codex_success(self):
244
+ """Test successful navigation to Codex."""
245
+ automation = CodexGitHubMentionsAutomation()
246
+ automation.page = AsyncMock()
247
+ automation.page.is_closed = Mock(return_value=False)
248
+
249
+ await automation.navigate_to_codex()
250
+
251
+ automation.page.goto.assert_called_once()
252
+ print("✅ Navigation to Codex successful")
253
+
254
+ @pytest.mark.asyncio
255
+ async def test_navigate_timeout_handled(self):
256
+ """Test that navigation timeout is handled gracefully."""
257
+ automation = CodexGitHubMentionsAutomation()
258
+ automation.page = AsyncMock()
259
+ automation.page.is_closed = Mock(return_value=False)
260
+ automation.page.goto.side_effect = TimeoutError("Navigation timeout")
261
+
262
+ with pytest.raises(TimeoutError):
263
+ await automation.navigate_to_codex()
264
+
265
+ print("✅ Navigation timeout raised correctly")
266
+
267
+ @pytest.mark.asyncio
268
+ async def test_click_task_success(self):
269
+ """Test clicking task link successfully."""
270
+ automation = CodexGitHubMentionsAutomation()
271
+
272
+ # Create mock for the button locator (after .first)
273
+ mock_button = AsyncMock()
274
+ mock_button.count = AsyncMock(return_value=1)
275
+ mock_button.click = AsyncMock()
276
+
277
+ # Create mock for the main locator that returns the button when .first is accessed
278
+ mock_locator = Mock()
279
+ mock_locator.first = mock_button
280
+ mock_locator.count = AsyncMock(return_value=1)
281
+
282
+ automation.page = AsyncMock()
283
+ automation.page.is_closed = Mock(return_value=False)
284
+ automation.page.goto = AsyncMock()
285
+ automation.page.locator = Mock(return_value=mock_locator)
286
+
287
+ task = {"href": "/codex/123", "text": "Test Task"}
288
+ result = await automation.update_pr_for_task(task)
289
+
290
+ assert result is True
291
+ automation.page.goto.assert_called()
292
+ mock_button.click.assert_awaited()
293
+ print("✅ Task navigation and update simulated successfully")
294
+
295
+ @pytest.mark.asyncio
296
+ async def test_find_button_when_present(self):
297
+ """Test finding Update branch button when present."""
298
+ automation = CodexGitHubMentionsAutomation()
299
+ automation.page = AsyncMock()
300
+ automation.page.is_closed = Mock(return_value=False)
301
+
302
+ mock_first = Mock()
303
+ mock_first.count = AsyncMock(return_value=1)
304
+ mock_locator = Mock()
305
+ mock_locator.first = mock_first
306
+ mock_locator.count = AsyncMock(return_value=1)
307
+ automation.page.locator = Mock(return_value=mock_locator)
308
+
309
+ # Simulate button check
310
+ button = automation.page.locator('button:has-text("Update branch")').first
311
+ count = await button.count()
312
+
313
+ assert count > 0
314
+ print("✅ Update branch button found")
315
+
316
+ @pytest.mark.asyncio
317
+ async def test_missing_button_handled(self):
318
+ """Test handling when Update branch button is missing."""
319
+ automation = CodexGitHubMentionsAutomation()
320
+ automation.page = AsyncMock()
321
+ automation.page.is_closed = Mock(return_value=False)
322
+
323
+ mock_locator = Mock()
324
+ mock_locator.count = AsyncMock(return_value=0)
325
+ automation.page.locator = Mock(return_value=mock_locator)
326
+
327
+ button_locator = automation.page.locator('button:has-text("Update branch")')
328
+ count = await button_locator.count()
329
+
330
+ assert count == 0
331
+ print("✅ Missing button handled correctly")
332
+
333
+ @requires_chrome
334
+ @pytest.mark.asyncio
335
+ async def test_complete_workflow_with_real_chrome(self):
336
+ """Test complete workflow with real Chrome instance."""
337
+ automation = CodexGitHubMentionsAutomation(task_limit=5)
338
+
339
+ # Connect
340
+ connected = await automation.connect_to_existing_browser()
341
+ assert connected is True
342
+
343
+ # Navigate
344
+ await automation.navigate_to_codex()
345
+
346
+ # Find tasks
347
+ tasks = await automation.find_github_mention_tasks()
348
+ assert isinstance(tasks, list)
349
+ assert len(tasks) <= 5
350
+
351
+ print(f"✅ Complete workflow successful with {len(tasks)} tasks found")
352
+
353
+
354
+ if __name__ == "__main__":
355
+ pytest.main([__file__, "-v", "-s"])
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Integration tests for Codex GitHub Mentions automation.
4
+
5
+ These tests use minimal mocking and test against a real Chrome instance
6
+ when available. Tests will skip gracefully if Chrome is not running.
7
+
8
+ Run with:
9
+ pytest automation/openai_automation/test_codex_integration.py -v
10
+
11
+ Or with Chrome running:
12
+ # Terminal 1
13
+ ./automation/openai_automation/start_chrome_debug.sh
14
+
15
+ # Terminal 2
16
+ pytest automation/openai_automation/test_codex_integration.py -v
17
+ """
18
+
19
+ import asyncio
20
+
21
+ import aiohttp
22
+ import pytest
23
+ from playwright.async_api import async_playwright
24
+
25
+ from jleechanorg_pr_automation.openai_automation.codex_github_mentions import (
26
+ CodexGitHubMentionsAutomation,
27
+ )
28
+
29
+
30
+ # Helper to check if Chrome is running with CDP
31
+ async def chrome_is_running(port=9222):
32
+ """Check if Chrome is running with remote debugging."""
33
+ try:
34
+ async with aiohttp.ClientSession() as session:
35
+ async with session.get(
36
+ f"http://localhost:{port}/json/version", timeout=aiohttp.ClientTimeout(total=1)
37
+ ) as resp:
38
+ return resp.status == 200
39
+ except (aiohttp.ClientError, asyncio.TimeoutError, OSError):
40
+ return False
41
+
42
+
43
+ # Fixture to check Chrome availability
44
+ @pytest.fixture(scope="session")
45
+ def chrome_available():
46
+ """Check if Chrome with CDP is available."""
47
+ return asyncio.run(chrome_is_running())
48
+
49
+
50
+ # Skip marker for tests requiring Chrome
51
+ requires_chrome = pytest.mark.skipif(
52
+ not asyncio.run(chrome_is_running()),
53
+ reason="Chrome with remote debugging not running on port 9222"
54
+ )
55
+
56
+
57
+ class TestCDPConnection:
58
+ """Test Chrome DevTools Protocol connection."""
59
+
60
+ @requires_chrome
61
+ @pytest.mark.asyncio
62
+ async def test_can_connect_to_chrome(self):
63
+ """Test basic CDP connection to Chrome."""
64
+ playwright = await async_playwright().start()
65
+
66
+ try:
67
+ browser = await playwright.chromium.connect_over_cdp("http://localhost:9222")
68
+ assert browser is not None
69
+ assert browser.version is not None
70
+ print(f"✅ Connected to Chrome {browser.version}")
71
+
72
+ finally:
73
+ await playwright.stop()
74
+
75
+ @requires_chrome
76
+ @pytest.mark.asyncio
77
+ async def test_can_access_context_and_pages(self):
78
+ """Test accessing browser contexts and pages."""
79
+ playwright = await async_playwright().start()
80
+
81
+ try:
82
+ browser = await playwright.chromium.connect_over_cdp("http://localhost:9222")
83
+ contexts = browser.contexts
84
+ assert len(contexts) > 0, "Should have at least one context"
85
+
86
+ context = contexts[0]
87
+ # Get or create a page
88
+ if context.pages:
89
+ page = context.pages[0]
90
+ else:
91
+ page = await context.new_page()
92
+
93
+ assert page is not None
94
+ title = await page.title()
95
+ print(f"✅ Got page with title: {title}")
96
+
97
+ finally:
98
+ await playwright.stop()
99
+
100
+
101
+ class TestCodexAutomation:
102
+ """Test Codex automation functionality."""
103
+
104
+ @requires_chrome
105
+ @pytest.mark.asyncio
106
+ async def test_can_navigate_to_codex(self):
107
+ """Test navigation to Codex page."""
108
+ playwright = await async_playwright().start()
109
+
110
+ try:
111
+ browser = await playwright.chromium.connect_over_cdp("http://localhost:9222")
112
+ context = browser.contexts[0]
113
+ page = context.pages[0] if context.pages else await context.new_page()
114
+
115
+ # Navigate to Codex
116
+ await page.goto("https://chatgpt.com/codex", wait_until="domcontentloaded", timeout=30000)
117
+ await asyncio.sleep(3)
118
+
119
+ title = await page.title()
120
+ assert "Codex" in title or "ChatGPT" in title
121
+ print(f"✅ Navigated to Codex page")
122
+
123
+ finally:
124
+ await playwright.stop()
125
+
126
+ @requires_chrome
127
+ @pytest.mark.asyncio
128
+ async def test_can_find_github_mention_tasks(self):
129
+ """Test finding GitHub Mention tasks on Codex page."""
130
+ playwright = await async_playwright().start()
131
+
132
+ try:
133
+ browser = await playwright.chromium.connect_over_cdp("http://localhost:9222")
134
+ context = browser.contexts[0]
135
+ page = context.pages[0] if context.pages else await context.new_page()
136
+
137
+ # Navigate to Codex
138
+ await page.goto("https://chatgpt.com/codex", wait_until="domcontentloaded", timeout=30000)
139
+ await asyncio.sleep(5) # Wait for dynamic content
140
+
141
+ # Find GitHub Mention tasks
142
+ task_links = await page.locator('a:has-text("GitHub Mention:")').all()
143
+
144
+ # We may or may not have tasks at any given time
145
+ print(f"✅ Found {len(task_links)} GitHub Mention tasks")
146
+ assert isinstance(task_links, list)
147
+
148
+ finally:
149
+ await playwright.stop()
150
+
151
+ @requires_chrome
152
+ @pytest.mark.asyncio
153
+ async def test_can_click_task_and_find_button(self):
154
+ """Test clicking a task and looking for Update branch button."""
155
+ playwright = await async_playwright().start()
156
+
157
+ try:
158
+ browser = await playwright.chromium.connect_over_cdp("http://localhost:9222")
159
+ context = browser.contexts[0]
160
+ page = context.pages[0] if context.pages else await context.new_page()
161
+
162
+ # Navigate to Codex
163
+ await page.goto("https://chatgpt.com/codex", wait_until="domcontentloaded", timeout=30000)
164
+ await asyncio.sleep(5)
165
+
166
+ # Find tasks
167
+ task_links = await page.locator('a:has-text("GitHub Mention:")').all()
168
+
169
+ if len(task_links) > 0:
170
+ # Click first task
171
+ task_text = await task_links[0].text_content()
172
+ print(f"Testing with task: {task_text[:50]}...")
173
+
174
+ await task_links[0].click()
175
+ await asyncio.sleep(3)
176
+
177
+ # Look for Update branch button
178
+ update_btn = page.locator('button:has-text("Update branch")').first
179
+ button_count = await update_btn.count()
180
+
181
+ print(f"✅ Task opened, Update branch button present: {button_count > 0}")
182
+
183
+ # Navigate back
184
+ await page.goto("https://chatgpt.com/codex", wait_until="domcontentloaded", timeout=30000)
185
+ else:
186
+ print("⚠️ No tasks available to test with")
187
+ pytest.skip("No GitHub Mention tasks available")
188
+
189
+ finally:
190
+ await playwright.stop()
191
+
192
+
193
+ class TestCodexAutomationClass:
194
+ """Test the CodexGitHubMentionsAutomation class directly."""
195
+
196
+ @requires_chrome
197
+ @pytest.mark.asyncio
198
+ async def test_automation_class_can_connect(self):
199
+ """Test that automation class can connect to Chrome."""
200
+ automation = CodexGitHubMentionsAutomation(cdp_url="http://localhost:9222")
201
+
202
+ try:
203
+ connected = await automation.connect_to_existing_browser()
204
+ assert connected is True
205
+ assert automation.browser is not None
206
+ assert automation.page is not None
207
+ print(f"✅ Automation class connected successfully")
208
+ finally:
209
+ # Cleanup
210
+ pass
211
+
212
+ @requires_chrome
213
+ @pytest.mark.asyncio
214
+ async def test_automation_can_navigate_to_codex(self):
215
+ """Test that automation class can navigate to Codex."""
216
+ automation = CodexGitHubMentionsAutomation(cdp_url="http://localhost:9222")
217
+
218
+ try:
219
+ await automation.connect_to_existing_browser()
220
+ await automation.navigate_to_codex()
221
+
222
+ title = await automation.page.title()
223
+ # Allow for loading pages ("Just a moment...") from Cloudflare
224
+ assert title is not None and len(title) > 0
225
+ print(f"✅ Automation navigated to Codex (title: {title})")
226
+ finally:
227
+ pass
228
+
229
+ @requires_chrome
230
+ @pytest.mark.asyncio
231
+ async def test_automation_can_find_tasks(self):
232
+ """Test that automation class can find GitHub Mention tasks."""
233
+ automation = CodexGitHubMentionsAutomation(cdp_url="http://localhost:9222")
234
+
235
+ try:
236
+ await automation.connect_to_existing_browser()
237
+ await automation.navigate_to_codex()
238
+
239
+ tasks = await automation.find_github_mention_tasks()
240
+ assert isinstance(tasks, list)
241
+ print(f"✅ Automation found {len(tasks)} tasks")
242
+ finally:
243
+ pass
244
+
245
+
246
+ def test_chrome_not_required_placeholder():
247
+ """Placeholder test that always passes (doesn't require Chrome)."""
248
+ assert True
249
+ print("✅ Placeholder test passed (no Chrome required)")
250
+
251
+
252
+ if __name__ == "__main__":
253
+ # Run tests
254
+ pytest.main([__file__, "-v", "-s"])