kolega-code 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.
Files changed (171) hide show
  1. kolega_code/__init__.py +151 -0
  2. kolega_code/agent/__init__.py +42 -0
  3. kolega_code/agent/baseagent.py +998 -0
  4. kolega_code/agent/browseragent.py +123 -0
  5. kolega_code/agent/coder.py +157 -0
  6. kolega_code/agent/common.py +41 -0
  7. kolega_code/agent/compression.py +81 -0
  8. kolega_code/agent/context.py +112 -0
  9. kolega_code/agent/conversation.py +408 -0
  10. kolega_code/agent/generalagent.py +146 -0
  11. kolega_code/agent/investigationagent.py +123 -0
  12. kolega_code/agent/planningagent.py +187 -0
  13. kolega_code/agent/prompt_provider.py +196 -0
  14. kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
  15. kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
  16. kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
  17. kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
  18. kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
  19. kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
  20. kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
  21. kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
  22. kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
  23. kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
  24. kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
  25. kolega_code/agent/prompts.py +192 -0
  26. kolega_code/agent/tests/__init__.py +0 -0
  27. kolega_code/agent/tests/llm/__init__.py +0 -0
  28. kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
  29. kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
  30. kolega_code/agent/tests/llm/test_client.py +773 -0
  31. kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
  32. kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
  33. kolega_code/agent/tests/llm/test_exceptions.py +249 -0
  34. kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
  35. kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
  36. kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
  37. kolega_code/agent/tests/llm/test_model_specs.py +17 -0
  38. kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
  39. kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
  40. kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
  41. kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
  42. kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
  43. kolega_code/agent/tests/services/__init__.py +1 -0
  44. kolega_code/agent/tests/services/test_browser.py +447 -0
  45. kolega_code/agent/tests/services/test_browser_parity.py +353 -0
  46. kolega_code/agent/tests/services/test_file_system.py +699 -0
  47. kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
  48. kolega_code/agent/tests/services/test_terminal.py +154 -0
  49. kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
  50. kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
  51. kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
  52. kolega_code/agent/tests/test_base_agent.py +1942 -0
  53. kolega_code/agent/tests/test_coder_attachments.py +330 -0
  54. kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
  55. kolega_code/agent/tests/test_commands.py +179 -0
  56. kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
  57. kolega_code/agent/tests/test_empty_message_handling.py +48 -0
  58. kolega_code/agent/tests/test_general_agent.py +242 -0
  59. kolega_code/agent/tests/test_html.py +320 -0
  60. kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
  61. kolega_code/agent/tests/test_planning_agent.py +227 -0
  62. kolega_code/agent/tests/test_prompt_provider.py +271 -0
  63. kolega_code/agent/tests/test_tool_registry.py +102 -0
  64. kolega_code/agent/tests/test_tools.py +549 -0
  65. kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  66. kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
  67. kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
  68. kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
  69. kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
  70. kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
  71. kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
  72. kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
  73. kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
  74. kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
  75. kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
  76. kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
  77. kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
  78. kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
  79. kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
  80. kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
  81. kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
  82. kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
  83. kolega_code/agent/tool_backend/agent_tool.py +414 -0
  84. kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
  85. kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
  86. kolega_code/agent/tool_backend/base_tool.py +217 -0
  87. kolega_code/agent/tool_backend/browser_tool.py +271 -0
  88. kolega_code/agent/tool_backend/build_tool.py +93 -0
  89. kolega_code/agent/tool_backend/create_file_tool.py +52 -0
  90. kolega_code/agent/tool_backend/glob_tool.py +323 -0
  91. kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
  92. kolega_code/agent/tool_backend/memory_tool.py +79 -0
  93. kolega_code/agent/tool_backend/read_file_tool.py +119 -0
  94. kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
  95. kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
  96. kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
  97. kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
  98. kolega_code/agent/tool_backend/streaming_tool.py +47 -0
  99. kolega_code/agent/tool_backend/terminal_tool.py +643 -0
  100. kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
  101. kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
  102. kolega_code/agent/tools.py +1704 -0
  103. kolega_code/agent/utils/commands.py +94 -0
  104. kolega_code/cli/__init__.py +1 -0
  105. kolega_code/cli/app.py +2756 -0
  106. kolega_code/cli/config.py +280 -0
  107. kolega_code/cli/connection.py +49 -0
  108. kolega_code/cli/file_index.py +147 -0
  109. kolega_code/cli/main.py +564 -0
  110. kolega_code/cli/mentions.py +155 -0
  111. kolega_code/cli/messages.py +89 -0
  112. kolega_code/cli/provider_registry.py +96 -0
  113. kolega_code/cli/session_store.py +207 -0
  114. kolega_code/cli/settings.py +87 -0
  115. kolega_code/cli/skills.py +409 -0
  116. kolega_code/cli/slash_commands.py +108 -0
  117. kolega_code/cli/tests/__init__.py +1 -0
  118. kolega_code/cli/tests/test_app.py +4251 -0
  119. kolega_code/cli/tests/test_cli_config.py +171 -0
  120. kolega_code/cli/tests/test_connection.py +26 -0
  121. kolega_code/cli/tests/test_file_index.py +103 -0
  122. kolega_code/cli/tests/test_main.py +455 -0
  123. kolega_code/cli/tests/test_mentions.py +108 -0
  124. kolega_code/cli/tests/test_session_store.py +67 -0
  125. kolega_code/cli/tests/test_settings.py +62 -0
  126. kolega_code/cli/tests/test_skills.py +157 -0
  127. kolega_code/cli/tests/test_slash_commands.py +88 -0
  128. kolega_code/cli/theme.py +180 -0
  129. kolega_code/config.py +154 -0
  130. kolega_code/events.py +202 -0
  131. kolega_code/llm/client.py +300 -0
  132. kolega_code/llm/exceptions.py +285 -0
  133. kolega_code/llm/instrumented_client.py +520 -0
  134. kolega_code/llm/models.py +1368 -0
  135. kolega_code/llm/providers/__init__.py +0 -0
  136. kolega_code/llm/providers/anthropic.py +387 -0
  137. kolega_code/llm/providers/base.py +71 -0
  138. kolega_code/llm/providers/google.py +157 -0
  139. kolega_code/llm/providers/models.py +37 -0
  140. kolega_code/llm/providers/openai.py +363 -0
  141. kolega_code/llm/ratelimit.py +40 -0
  142. kolega_code/llm/specs.py +67 -0
  143. kolega_code/llm/tool_execution_ids.py +18 -0
  144. kolega_code/models/__init__.py +9 -0
  145. kolega_code/models/sandbox_terminal_state.py +47 -0
  146. kolega_code/runtime.py +50 -0
  147. kolega_code/sandbox/README.md +200 -0
  148. kolega_code/sandbox/__init__.py +21 -0
  149. kolega_code/sandbox/async_filesystem.py +475 -0
  150. kolega_code/sandbox/base.py +297 -0
  151. kolega_code/sandbox/browser.py +25 -0
  152. kolega_code/sandbox/event_loop.py +43 -0
  153. kolega_code/sandbox/filesystem.py +341 -0
  154. kolega_code/sandbox/local.py +118 -0
  155. kolega_code/sandbox/serializer.py +175 -0
  156. kolega_code/sandbox/terminal.py +868 -0
  157. kolega_code/sandbox/utils.py +216 -0
  158. kolega_code/services/base.py +255 -0
  159. kolega_code/services/browser.py +444 -0
  160. kolega_code/services/file_system.py +749 -0
  161. kolega_code/services/html.py +221 -0
  162. kolega_code/services/terminal.py +903 -0
  163. kolega_code/tools/__init__.py +22 -0
  164. kolega_code/tools/core.py +33 -0
  165. kolega_code/tools/definitions.py +81 -0
  166. kolega_code/tools/registry.py +73 -0
  167. kolega_code-0.1.0.dist-info/METADATA +157 -0
  168. kolega_code-0.1.0.dist-info/RECORD +171 -0
  169. kolega_code-0.1.0.dist-info/WHEEL +4 -0
  170. kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
  171. kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,353 @@
1
+ """Test parity between local and BrowserStack browser managers."""
2
+
3
+ import datetime
4
+ import os
5
+ import pytest
6
+ from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
7
+ from dotenv import load_dotenv
8
+
9
+ from kolega_code.services.browser import PlaywrightBrowserManager
10
+ from kolega_code.sandbox.browser import SandboxBrowserManager
11
+
12
+ # Load environment variables from .env file
13
+ load_dotenv()
14
+
15
+
16
+ class TestBrowserManagerParity:
17
+ """Test suite to ensure identical behavior between local and BrowserStack browser managers."""
18
+
19
+ @pytest.fixture
20
+ def mock_env_vars(self, monkeypatch):
21
+ """Mock environment variables for BrowserStack and Browserless."""
22
+ monkeypatch.setenv("BROWSERSTACK_USERNAME", "test_user")
23
+ monkeypatch.setenv("BROWSERSTACK_ACCESS_KEY", "test_key")
24
+ monkeypatch.setenv("BROWSERLESS_API_KEY", "test_browserless_key")
25
+
26
+ @pytest.fixture
27
+ def local_browser_manager(self):
28
+ """Create a local browser manager instance."""
29
+ return PlaywrightBrowserManager(browser_backend="local")
30
+
31
+ @pytest.fixture
32
+ def browserstack_browser_manager(self, mock_env_vars):
33
+ """Create a BrowserStack browser manager instance."""
34
+ return PlaywrightBrowserManager(browser_backend="browserstack")
35
+
36
+ @pytest.fixture
37
+ def browserless_browser_manager(self, mock_env_vars):
38
+ """Create a Browserless browser manager instance."""
39
+ return PlaywrightBrowserManager(browser_backend="browserless")
40
+
41
+ @pytest.fixture
42
+ def sandbox_browser_manager(self, mock_env_vars):
43
+ """Create a sandbox browser manager instance (uses Browserless)."""
44
+ return SandboxBrowserManager()
45
+
46
+ def test_initialization(self, local_browser_manager, browserstack_browser_manager, sandbox_browser_manager):
47
+ """Test that all managers initialize with the same properties."""
48
+ # Check common properties
49
+ assert local_browser_manager.viewport == browserstack_browser_manager.viewport
50
+ assert local_browser_manager.viewport == sandbox_browser_manager.viewport
51
+
52
+ assert local_browser_manager.user_agent == browserstack_browser_manager.user_agent
53
+ assert local_browser_manager.user_agent == sandbox_browser_manager.user_agent
54
+
55
+ assert local_browser_manager.headless == browserstack_browser_manager.headless
56
+ assert local_browser_manager.headless == sandbox_browser_manager.headless
57
+
58
+ assert local_browser_manager.interaction_timeout == browserstack_browser_manager.interaction_timeout
59
+ assert local_browser_manager.interaction_timeout == sandbox_browser_manager.interaction_timeout
60
+
61
+ assert (
62
+ local_browser_manager.max_console_logs_per_browser
63
+ == browserstack_browser_manager.max_console_logs_per_browser
64
+ )
65
+ assert (
66
+ local_browser_manager.max_console_logs_per_browser == sandbox_browser_manager.max_console_logs_per_browser
67
+ )
68
+
69
+ # Check backend-specific properties
70
+ assert local_browser_manager.browser_backend == "local"
71
+ assert browserstack_browser_manager.browser_backend == "browserstack"
72
+ assert sandbox_browser_manager.browser_backend == "browserless"
73
+
74
+ def test_browserstack_credentials_required(self):
75
+ """Test that BrowserStack and Browserless managers require credentials."""
76
+ # Clear environment variables
77
+ os.environ.pop("BROWSERSTACK_USERNAME", None)
78
+ os.environ.pop("BROWSERSTACK_ACCESS_KEY", None)
79
+ os.environ.pop("BROWSERLESS_API_KEY", None)
80
+
81
+ # Should raise ValueError without BrowserStack credentials
82
+ with pytest.raises(ValueError, match="BrowserStack credentials not found"):
83
+ PlaywrightBrowserManager(browser_backend="browserstack")
84
+
85
+ # Should raise ValueError without Browserless credentials
86
+ with pytest.raises(ValueError, match="Browserless API key not found"):
87
+ PlaywrightBrowserManager(browser_backend="browserless")
88
+
89
+ # SandboxBrowserManager uses Browserless, so should fail without Browserless credentials
90
+ with pytest.raises(ValueError, match="Browserless API key not found"):
91
+ SandboxBrowserManager()
92
+
93
+ def test_backward_compatibility(self, mock_env_vars):
94
+ """Test backward compatibility with use_browserstack parameter."""
95
+ # Using use_browserstack=True should set browser_backend to "browserstack"
96
+ manager = PlaywrightBrowserManager(use_browserstack=True)
97
+ assert manager.browser_backend == "browserstack"
98
+
99
+ # Using use_browserstack=False should keep default backend
100
+ manager = PlaywrightBrowserManager(use_browserstack=False)
101
+ assert manager.browser_backend == "local"
102
+
103
+ @pytest.mark.asyncio
104
+ async def test_launch_browser_interface(self, local_browser_manager, browserstack_browser_manager):
105
+ """Test that launch_browser has the same interface for both managers."""
106
+ # Mock the playwright async_playwright
107
+ with patch("kolega_code.services.browser.async_playwright") as mock_playwright:
108
+ # Setup mock playwright - fix the async mock issue
109
+ mock_pw_instance = MagicMock()
110
+ mock_async_playwright = AsyncMock()
111
+ mock_async_playwright.start.return_value = mock_pw_instance
112
+ mock_playwright.return_value = mock_async_playwright
113
+
114
+ # Mock browser and page
115
+ mock_browser = AsyncMock()
116
+ mock_context = AsyncMock()
117
+ mock_page = AsyncMock()
118
+
119
+ mock_browser.new_context.return_value = mock_context
120
+ mock_context.new_page.return_value = mock_page
121
+ mock_page.url = "https://example.com"
122
+ mock_page.evaluate = AsyncMock()
123
+ mock_page.goto = AsyncMock()
124
+ mock_page.on = MagicMock()
125
+
126
+ # For local browser
127
+ mock_pw_instance.chromium = MagicMock()
128
+ mock_pw_instance.chromium.launch = AsyncMock(return_value=mock_browser)
129
+
130
+ # For BrowserStack browser - mock connect() instead of connect_over_cdp()
131
+ mock_pw_instance.chromium.connect = AsyncMock(return_value=mock_browser)
132
+
133
+ # Test local browser
134
+ local_result = await local_browser_manager.launch_browser("https://example.com")
135
+ assert local_result is not None
136
+ assert isinstance(local_result, str) # Should return browser ID
137
+
138
+ # Test BrowserStack browser
139
+ bs_result = await browserstack_browser_manager.launch_browser("https://example.com")
140
+ assert bs_result is not None
141
+ assert isinstance(bs_result, str) # Should return browser ID
142
+
143
+ # Verify different connection methods were used
144
+ mock_pw_instance.chromium.launch.assert_called_once()
145
+ mock_pw_instance.chromium.connect.assert_called_once()
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_browser_info_structure(self, local_browser_manager, browserstack_browser_manager):
149
+ """Test that browser info structure is identical for both managers."""
150
+ # Mock the playwright async_playwright
151
+ with patch("kolega_code.services.browser.async_playwright") as mock_playwright:
152
+ # Setup mock playwright - fix the async mock issue
153
+ mock_pw_instance = MagicMock()
154
+ mock_async_playwright = AsyncMock()
155
+ mock_async_playwright.start.return_value = mock_pw_instance
156
+ mock_playwright.return_value = mock_async_playwright
157
+
158
+ # Mock browser and page
159
+ mock_browser = AsyncMock()
160
+ mock_context = AsyncMock()
161
+ mock_page = AsyncMock()
162
+
163
+ mock_browser.new_context.return_value = mock_context
164
+ mock_context.new_page.return_value = mock_page
165
+ mock_page.url = "https://example.com"
166
+ mock_page.evaluate = AsyncMock()
167
+ mock_page.goto = AsyncMock()
168
+ mock_page.on = MagicMock()
169
+
170
+ # For both browser types
171
+ mock_pw_instance.chromium = MagicMock()
172
+ mock_pw_instance.chromium.launch = AsyncMock(return_value=mock_browser)
173
+ mock_pw_instance.chromium.connect = AsyncMock(return_value=mock_browser)
174
+
175
+ # Launch browsers
176
+ local_id = await local_browser_manager.launch_browser("https://example.com")
177
+ bs_id = await browserstack_browser_manager.launch_browser("https://example.com")
178
+
179
+ # Check browser info structure
180
+ local_info = local_browser_manager.browsers[local_id]
181
+ bs_info = browserstack_browser_manager.browsers[bs_id]
182
+
183
+ # Both should have the same keys
184
+ assert set(local_info.keys()) == set(bs_info.keys())
185
+
186
+ # Check specific fields
187
+ assert local_info["type"] == bs_info["type"] == "chromium"
188
+ assert local_info["url"] == bs_info["url"] == "https://example.com"
189
+ assert "playwright" in local_info and "playwright" in bs_info
190
+ assert "browser" in local_info and "browser" in bs_info
191
+ assert "context" in local_info and "context" in bs_info
192
+ assert "page" in local_info and "page" in bs_info
193
+ assert "console_logs" in local_info and "console_logs" in bs_info
194
+ assert "network_requests" in local_info and "network_requests" in bs_info
195
+ assert "launched_at" in local_info and "launched_at" in bs_info
196
+
197
+ # BrowserStack flag should be different
198
+ assert local_info["browserstack"] is False
199
+ assert bs_info["browserstack"] is True
200
+
201
+ # Backend field should be different
202
+ assert local_info["backend"] == "local"
203
+ assert bs_info["backend"] == "browserstack"
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_console_log_handling(self, local_browser_manager, browserstack_browser_manager):
207
+ """Test that console log handling is identical for both managers."""
208
+ # Create mock browser info with console logs
209
+ now = datetime.datetime.now()
210
+ console_logs = [
211
+ {
212
+ "type": "error",
213
+ "text": "Test error",
214
+ "timestamp": now.isoformat(),
215
+ "location": None,
216
+ }
217
+ ]
218
+
219
+ browser_id = "test-browser-id"
220
+ mock_browser_info = {
221
+ "console_logs": console_logs,
222
+ "launched_at": now.isoformat(),
223
+ }
224
+
225
+ # Add to both managers
226
+ local_browser_manager.browsers[browser_id] = mock_browser_info.copy()
227
+ browserstack_browser_manager.browsers[browser_id] = mock_browser_info.copy()
228
+
229
+ # Test console log retrieval
230
+ local_logs = await local_browser_manager.get_browser_console_logs(browser_id)
231
+ bs_logs = await browserstack_browser_manager.get_browser_console_logs(browser_id)
232
+
233
+ # Results should be identical
234
+ assert local_logs == bs_logs
235
+ assert local_logs["total_logs_count"] == 1
236
+ assert local_logs["returned_count"] == 1
237
+ assert local_logs["console_logs"][0]["type"] == "error"
238
+ assert local_logs["console_logs"][0]["text"] == "Test error"
239
+
240
+ @pytest.mark.asyncio
241
+ async def test_error_handling(self, local_browser_manager, browserstack_browser_manager):
242
+ """Test that error handling is consistent across both managers."""
243
+ # Test browser not found error
244
+ with pytest.raises(KeyError, match="Browser with ID nonexistent not found"):
245
+ await local_browser_manager.get_browser_console_logs("nonexistent")
246
+
247
+ with pytest.raises(KeyError, match="Browser with ID nonexistent not found"):
248
+ await browserstack_browser_manager.get_browser_console_logs("nonexistent")
249
+
250
+ # Test other methods with non-existent browser
251
+ with pytest.raises(KeyError):
252
+ await local_browser_manager.take_browser_screenshot("nonexistent")
253
+
254
+ with pytest.raises(KeyError):
255
+ await browserstack_browser_manager.take_browser_screenshot("nonexistent")
256
+
257
+ @pytest.mark.asyncio
258
+ async def test_sandbox_manager_inheritance(self, browserless_browser_manager, sandbox_browser_manager):
259
+ """Test that SandboxBrowserManager behaves as a PlaywrightBrowserManager with Browserless backend."""
260
+ # Check that they both use Browserless backend
261
+ assert browserless_browser_manager.browser_backend == "browserless"
262
+ assert sandbox_browser_manager.browser_backend == "browserless"
263
+
264
+ # Both managers should have browserless credentials
265
+ assert hasattr(browserless_browser_manager, "browserless_api_key")
266
+
267
+ # Sandbox manager should have browserless credentials
268
+ assert hasattr(sandbox_browser_manager, "browserless_api_key")
269
+
270
+ # SandboxBrowserManager should have its additional sandbox attribute
271
+ assert hasattr(sandbox_browser_manager, "sandbox")
272
+ assert not hasattr(browserless_browser_manager, "sandbox")
273
+
274
+ def test_cdp_url_generation(
275
+ self, browserstack_browser_manager, browserless_browser_manager, sandbox_browser_manager
276
+ ):
277
+ """Test that managers generate correct CDP URLs for their respective backends."""
278
+ # BrowserStack manager should generate BrowserStack URL
279
+ bs_url = browserstack_browser_manager._get_browserstack_cdp_url()
280
+ assert bs_url.startswith("wss://cdp.browserstack.com/playwright?caps=")
281
+ assert "browserstack.username" in bs_url
282
+ assert "browserstack.accessKey" in bs_url
283
+
284
+ # Browserless manager should generate Browserless URL
285
+ browserless_url = browserless_browser_manager._get_browserless_cdp_url()
286
+ assert browserless_url.startswith("wss://production-sfo.browserless.io?token=")
287
+ assert "timeout=" in browserless_url
288
+
289
+ # Sandbox manager should generate Browserless URL
290
+ sandbox_browserless_url = sandbox_browser_manager._get_browserless_cdp_url()
291
+ assert sandbox_browserless_url.startswith("wss://production-sfo.browserless.io?token=")
292
+ assert "timeout=" in sandbox_browserless_url
293
+
294
+ @pytest.mark.asyncio
295
+ async def test_list_browsers_parity(self, local_browser_manager, browserstack_browser_manager):
296
+ """Test that list_browsers returns the same structure for both managers."""
297
+ # Mock browser info
298
+ browser_info = {
299
+ "url": "https://example.com",
300
+ "launched_at": datetime.datetime.now().isoformat(),
301
+ "browserstack": False,
302
+ "backend": "local",
303
+ }
304
+
305
+ browser_id = "test-id"
306
+ local_browser_manager.browsers[browser_id] = {**browser_info, "browserstack": False, "backend": "local"}
307
+ browserstack_browser_manager.browsers[browser_id] = {
308
+ **browser_info,
309
+ "browserstack": True,
310
+ "backend": "browserstack",
311
+ }
312
+
313
+ local_list = await local_browser_manager.list_browsers()
314
+ bs_list = await browserstack_browser_manager.list_browsers()
315
+
316
+ # Check structure is the same
317
+ assert set(local_list[browser_id].keys()) == set(bs_list[browser_id].keys())
318
+ assert local_list[browser_id]["url"] == bs_list[browser_id]["url"]
319
+ assert local_list[browser_id]["launched_at"] == bs_list[browser_id]["launched_at"]
320
+
321
+ # Backend flags should be different
322
+ assert local_list[browser_id]["browserstack"] is False
323
+ assert bs_list[browser_id]["browserstack"] is True
324
+ assert local_list[browser_id]["backend"] == "local"
325
+ assert bs_list[browser_id]["backend"] == "browserstack"
326
+
327
+ @pytest.mark.asyncio
328
+ async def test_all_methods_available(
329
+ self, local_browser_manager, browserstack_browser_manager, sandbox_browser_manager
330
+ ):
331
+ """Test that all browser manager methods are available on all implementations."""
332
+ methods = [
333
+ "launch_browser",
334
+ "list_browsers",
335
+ "get_browser_console_logs",
336
+ "get_browser_interactive_elements",
337
+ "get_browser_content",
338
+ "take_browser_screenshot",
339
+ "interact_with_browser",
340
+ "set_select_value",
341
+ "close_browser",
342
+ "cleanup_all_browsers",
343
+ ]
344
+
345
+ for method in methods:
346
+ assert hasattr(local_browser_manager, method)
347
+ assert hasattr(browserstack_browser_manager, method)
348
+ assert hasattr(sandbox_browser_manager, method)
349
+
350
+ # All methods should be callable
351
+ assert callable(getattr(local_browser_manager, method))
352
+ assert callable(getattr(browserstack_browser_manager, method))
353
+ assert callable(getattr(sandbox_browser_manager, method))