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,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"])
|