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.
- kolega_code/__init__.py +151 -0
- kolega_code/agent/__init__.py +42 -0
- kolega_code/agent/baseagent.py +998 -0
- kolega_code/agent/browseragent.py +123 -0
- kolega_code/agent/coder.py +157 -0
- kolega_code/agent/common.py +41 -0
- kolega_code/agent/compression.py +81 -0
- kolega_code/agent/context.py +112 -0
- kolega_code/agent/conversation.py +408 -0
- kolega_code/agent/generalagent.py +146 -0
- kolega_code/agent/investigationagent.py +123 -0
- kolega_code/agent/planningagent.py +187 -0
- kolega_code/agent/prompt_provider.py +196 -0
- kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
- kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
- kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
- kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
- kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
- kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
- kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
- kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
- kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
- kolega_code/agent/prompts.py +192 -0
- kolega_code/agent/tests/__init__.py +0 -0
- kolega_code/agent/tests/llm/__init__.py +0 -0
- kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
- kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
- kolega_code/agent/tests/llm/test_client.py +773 -0
- kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
- kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
- kolega_code/agent/tests/llm/test_exceptions.py +249 -0
- kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
- kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
- kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
- kolega_code/agent/tests/llm/test_model_specs.py +17 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
- kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
- kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
- kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
- kolega_code/agent/tests/services/__init__.py +1 -0
- kolega_code/agent/tests/services/test_browser.py +447 -0
- kolega_code/agent/tests/services/test_browser_parity.py +353 -0
- kolega_code/agent/tests/services/test_file_system.py +699 -0
- kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
- kolega_code/agent/tests/services/test_terminal.py +154 -0
- kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
- kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
- kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
- kolega_code/agent/tests/test_base_agent.py +1942 -0
- kolega_code/agent/tests/test_coder_attachments.py +330 -0
- kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
- kolega_code/agent/tests/test_commands.py +179 -0
- kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
- kolega_code/agent/tests/test_empty_message_handling.py +48 -0
- kolega_code/agent/tests/test_general_agent.py +242 -0
- kolega_code/agent/tests/test_html.py +320 -0
- kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
- kolega_code/agent/tests/test_planning_agent.py +227 -0
- kolega_code/agent/tests/test_prompt_provider.py +271 -0
- kolega_code/agent/tests/test_tool_registry.py +102 -0
- kolega_code/agent/tests/test_tools.py +549 -0
- kolega_code/agent/tests/tool_backend/__init__.py +0 -0
- kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
- kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
- kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
- kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
- kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
- kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
- kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
- kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
- kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
- kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
- kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
- kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
- kolega_code/agent/tool_backend/agent_tool.py +414 -0
- kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
- kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
- kolega_code/agent/tool_backend/base_tool.py +217 -0
- kolega_code/agent/tool_backend/browser_tool.py +271 -0
- kolega_code/agent/tool_backend/build_tool.py +93 -0
- kolega_code/agent/tool_backend/create_file_tool.py +52 -0
- kolega_code/agent/tool_backend/glob_tool.py +323 -0
- kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
- kolega_code/agent/tool_backend/memory_tool.py +79 -0
- kolega_code/agent/tool_backend/read_file_tool.py +119 -0
- kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
- kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
- kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
- kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
- kolega_code/agent/tool_backend/streaming_tool.py +47 -0
- kolega_code/agent/tool_backend/terminal_tool.py +643 -0
- kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
- kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
- kolega_code/agent/tools.py +1704 -0
- kolega_code/agent/utils/commands.py +94 -0
- kolega_code/cli/__init__.py +1 -0
- kolega_code/cli/app.py +2756 -0
- kolega_code/cli/config.py +280 -0
- kolega_code/cli/connection.py +49 -0
- kolega_code/cli/file_index.py +147 -0
- kolega_code/cli/main.py +564 -0
- kolega_code/cli/mentions.py +155 -0
- kolega_code/cli/messages.py +89 -0
- kolega_code/cli/provider_registry.py +96 -0
- kolega_code/cli/session_store.py +207 -0
- kolega_code/cli/settings.py +87 -0
- kolega_code/cli/skills.py +409 -0
- kolega_code/cli/slash_commands.py +108 -0
- kolega_code/cli/tests/__init__.py +1 -0
- kolega_code/cli/tests/test_app.py +4251 -0
- kolega_code/cli/tests/test_cli_config.py +171 -0
- kolega_code/cli/tests/test_connection.py +26 -0
- kolega_code/cli/tests/test_file_index.py +103 -0
- kolega_code/cli/tests/test_main.py +455 -0
- kolega_code/cli/tests/test_mentions.py +108 -0
- kolega_code/cli/tests/test_session_store.py +67 -0
- kolega_code/cli/tests/test_settings.py +62 -0
- kolega_code/cli/tests/test_skills.py +157 -0
- kolega_code/cli/tests/test_slash_commands.py +88 -0
- kolega_code/cli/theme.py +180 -0
- kolega_code/config.py +154 -0
- kolega_code/events.py +202 -0
- kolega_code/llm/client.py +300 -0
- kolega_code/llm/exceptions.py +285 -0
- kolega_code/llm/instrumented_client.py +520 -0
- kolega_code/llm/models.py +1368 -0
- kolega_code/llm/providers/__init__.py +0 -0
- kolega_code/llm/providers/anthropic.py +387 -0
- kolega_code/llm/providers/base.py +71 -0
- kolega_code/llm/providers/google.py +157 -0
- kolega_code/llm/providers/models.py +37 -0
- kolega_code/llm/providers/openai.py +363 -0
- kolega_code/llm/ratelimit.py +40 -0
- kolega_code/llm/specs.py +67 -0
- kolega_code/llm/tool_execution_ids.py +18 -0
- kolega_code/models/__init__.py +9 -0
- kolega_code/models/sandbox_terminal_state.py +47 -0
- kolega_code/runtime.py +50 -0
- kolega_code/sandbox/README.md +200 -0
- kolega_code/sandbox/__init__.py +21 -0
- kolega_code/sandbox/async_filesystem.py +475 -0
- kolega_code/sandbox/base.py +297 -0
- kolega_code/sandbox/browser.py +25 -0
- kolega_code/sandbox/event_loop.py +43 -0
- kolega_code/sandbox/filesystem.py +341 -0
- kolega_code/sandbox/local.py +118 -0
- kolega_code/sandbox/serializer.py +175 -0
- kolega_code/sandbox/terminal.py +868 -0
- kolega_code/sandbox/utils.py +216 -0
- kolega_code/services/base.py +255 -0
- kolega_code/services/browser.py +444 -0
- kolega_code/services/file_system.py +749 -0
- kolega_code/services/html.py +221 -0
- kolega_code/services/terminal.py +903 -0
- kolega_code/tools/__init__.py +22 -0
- kolega_code/tools/core.py +33 -0
- kolega_code/tools/definitions.py +81 -0
- kolega_code/tools/registry.py +73 -0
- kolega_code-0.1.0.dist-info/METADATA +157 -0
- kolega_code-0.1.0.dist-info/RECORD +171 -0
- kolega_code-0.1.0.dist-info/WHEEL +4 -0
- kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
- kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from kolega_code.llm.models import ContentBlock, Message, ToolCall
|
|
2
|
+
from kolega_code.llm.tool_execution_ids import ToolExecutionIdRegistry, new_tool_execution_id
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_new_tool_execution_id_uses_internal_prefix_and_is_unique():
|
|
6
|
+
first = new_tool_execution_id()
|
|
7
|
+
second = new_tool_execution_id()
|
|
8
|
+
|
|
9
|
+
assert first.startswith("tool_exec_")
|
|
10
|
+
assert second.startswith("tool_exec_")
|
|
11
|
+
assert first != second
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_tool_execution_id_registry_reuses_id_within_response():
|
|
15
|
+
registry = ToolExecutionIdRegistry()
|
|
16
|
+
|
|
17
|
+
first = registry.get_or_create("provider_tool_call_id")
|
|
18
|
+
second = registry.get_or_create("provider_tool_call_id")
|
|
19
|
+
|
|
20
|
+
assert first == second
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_tool_execution_id_registry_is_response_scoped():
|
|
24
|
+
first_registry = ToolExecutionIdRegistry()
|
|
25
|
+
second_registry = ToolExecutionIdRegistry()
|
|
26
|
+
|
|
27
|
+
first = first_registry.get_or_create("provider_tool_call_id")
|
|
28
|
+
second = second_registry.get_or_create("provider_tool_call_id")
|
|
29
|
+
|
|
30
|
+
assert first != second
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_tool_call_from_dict_preserves_existing_execution_id():
|
|
34
|
+
tool_call = ToolCall.from_dict(
|
|
35
|
+
{
|
|
36
|
+
"type": "tool_call",
|
|
37
|
+
"id": "provider_tool_call_id",
|
|
38
|
+
"name": "read_file",
|
|
39
|
+
"input": {"path": "README.md"},
|
|
40
|
+
"execution_id": "tool_exec_existing",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert tool_call.id == "provider_tool_call_id"
|
|
45
|
+
assert tool_call.execution_id == "tool_exec_existing"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_tool_call_from_dict_generates_execution_id_for_legacy_records():
|
|
49
|
+
tool_call = ToolCall.from_dict(
|
|
50
|
+
{
|
|
51
|
+
"type": "tool_call",
|
|
52
|
+
"id": "provider_tool_call_id",
|
|
53
|
+
"name": "read_file",
|
|
54
|
+
"input": {"path": "README.md"},
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert tool_call.id == "provider_tool_call_id"
|
|
59
|
+
assert tool_call.execution_id.startswith("tool_exec_")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_message_from_anthropic_uses_supplied_execution_id_registry():
|
|
63
|
+
class ToolUseBlock:
|
|
64
|
+
type = "tool_use"
|
|
65
|
+
id = "provider_tool_call_id"
|
|
66
|
+
name = "read_file"
|
|
67
|
+
input = {"path": "README.md"}
|
|
68
|
+
|
|
69
|
+
class AnthropicMessage:
|
|
70
|
+
role = "assistant"
|
|
71
|
+
stop_reason = "tool_use"
|
|
72
|
+
content = [ToolUseBlock()]
|
|
73
|
+
|
|
74
|
+
registry = ToolExecutionIdRegistry()
|
|
75
|
+
execution_id = registry.get_or_create("provider_tool_call_id")
|
|
76
|
+
|
|
77
|
+
message = Message.from_anthropic(AnthropicMessage(), tool_execution_ids=registry)
|
|
78
|
+
|
|
79
|
+
assert message.tool_calls[0].id == "provider_tool_call_id"
|
|
80
|
+
assert message.tool_calls[0].execution_id == execution_id
|
|
81
|
+
assert message.content[0].execution_id == execution_id
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_message_from_openai_uses_supplied_execution_id_registry():
|
|
85
|
+
class Function:
|
|
86
|
+
name = "read_file"
|
|
87
|
+
arguments = '{"path": "README.md"}'
|
|
88
|
+
|
|
89
|
+
class ToolCall:
|
|
90
|
+
id = "provider_tool_call_id"
|
|
91
|
+
function = Function()
|
|
92
|
+
|
|
93
|
+
class OpenAIMessage:
|
|
94
|
+
content = None
|
|
95
|
+
finish_reason = "tool_calls"
|
|
96
|
+
tool_calls = [ToolCall()]
|
|
97
|
+
|
|
98
|
+
registry = ToolExecutionIdRegistry()
|
|
99
|
+
execution_id = registry.get_or_create("provider_tool_call_id")
|
|
100
|
+
|
|
101
|
+
message = Message.from_openai(OpenAIMessage(), tool_execution_ids=registry)
|
|
102
|
+
|
|
103
|
+
assert message.tool_calls[0].id == "provider_tool_call_id"
|
|
104
|
+
assert message.tool_calls[0].execution_id == execution_id
|
|
105
|
+
assert message.content[0].execution_id == execution_id
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_message_from_google_uses_supplied_execution_id_registry():
|
|
109
|
+
class Content:
|
|
110
|
+
parts = []
|
|
111
|
+
|
|
112
|
+
class Candidate:
|
|
113
|
+
content = Content()
|
|
114
|
+
|
|
115
|
+
class FunctionCall:
|
|
116
|
+
id = "provider_tool_call_id"
|
|
117
|
+
name = "read_file"
|
|
118
|
+
args = {"path": "README.md"}
|
|
119
|
+
|
|
120
|
+
class GoogleMessage:
|
|
121
|
+
candidates = [Candidate()]
|
|
122
|
+
function_calls = [FunctionCall()]
|
|
123
|
+
finish_reason = "STOP"
|
|
124
|
+
|
|
125
|
+
registry = ToolExecutionIdRegistry()
|
|
126
|
+
execution_id = registry.get_or_create("provider_tool_call_id")
|
|
127
|
+
|
|
128
|
+
message = Message.from_google(GoogleMessage(), tool_execution_ids=registry)
|
|
129
|
+
|
|
130
|
+
assert message.tool_calls[0].id == "provider_tool_call_id"
|
|
131
|
+
assert message.tool_calls[0].execution_id == execution_id
|
|
132
|
+
assert message.content == []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_message_from_openai_stream_uses_supplied_execution_id_registry():
|
|
136
|
+
class Function:
|
|
137
|
+
name = "read_file"
|
|
138
|
+
arguments = '{"path": "README.md"}'
|
|
139
|
+
|
|
140
|
+
class ToolCall:
|
|
141
|
+
id = "provider_tool_call_id"
|
|
142
|
+
function = Function()
|
|
143
|
+
|
|
144
|
+
registry = ToolExecutionIdRegistry()
|
|
145
|
+
execution_id = registry.get_or_create("provider_tool_call_id")
|
|
146
|
+
|
|
147
|
+
message = Message.from_openai_stream(
|
|
148
|
+
role="assistant",
|
|
149
|
+
content="",
|
|
150
|
+
tool_calls={0: ToolCall()},
|
|
151
|
+
stop_reason="tool_calls",
|
|
152
|
+
tool_execution_ids=registry,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
assert message.tool_calls[0].id == "provider_tool_call_id"
|
|
156
|
+
assert message.tool_calls[0].execution_id == execution_id
|
|
157
|
+
assert message.content[0].execution_id == execution_id
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_message_from_google_stream_uses_supplied_execution_id_registry():
|
|
161
|
+
class ToolCall:
|
|
162
|
+
id = "provider_tool_call_id"
|
|
163
|
+
name = "read_file"
|
|
164
|
+
args = {"path": "README.md"}
|
|
165
|
+
|
|
166
|
+
registry = ToolExecutionIdRegistry()
|
|
167
|
+
execution_id = registry.get_or_create("provider_tool_call_id")
|
|
168
|
+
|
|
169
|
+
message = Message.from_google_stream(
|
|
170
|
+
role="assistant",
|
|
171
|
+
content="",
|
|
172
|
+
tool_calls={0: ToolCall()},
|
|
173
|
+
stop_reason="STOP",
|
|
174
|
+
tool_execution_ids=registry,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
assert message.tool_calls[0].id == "provider_tool_call_id"
|
|
178
|
+
assert message.tool_calls[0].execution_id == execution_id
|
|
179
|
+
assert message.content[0].execution_id == execution_id
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_content_block_from_dict_generates_execution_id_for_legacy_tool_call():
|
|
183
|
+
block = ContentBlock.from_dict(
|
|
184
|
+
{
|
|
185
|
+
"type": "tool_call",
|
|
186
|
+
"id": "provider_tool_call_id",
|
|
187
|
+
"name": "read_file",
|
|
188
|
+
"input": {"path": "README.md"},
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert isinstance(block, ToolCall)
|
|
193
|
+
assert block.execution_id.startswith("tool_exec_")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Services test package
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
5
|
+
from kolega_code.services.browser import PlaywrightBrowserManager
|
|
6
|
+
|
|
7
|
+
# Check if running in CI environment
|
|
8
|
+
SKIP_IN_CI = bool(os.getenv("CI")) or bool(os.getenv("GITLAB_CI"))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestPlaywrightBrowserManager:
|
|
12
|
+
"""Test suite for PlaywrightBrowserManager console log filtering functionality."""
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def browser_manager(self):
|
|
16
|
+
"""Create a browser manager instance for testing."""
|
|
17
|
+
return PlaywrightBrowserManager()
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def mock_browser_info(self):
|
|
21
|
+
"""Create mock browser info with sample console logs."""
|
|
22
|
+
now = datetime.datetime.now()
|
|
23
|
+
console_logs = [
|
|
24
|
+
{
|
|
25
|
+
"type": "log",
|
|
26
|
+
"text": "Regular log message 1",
|
|
27
|
+
"timestamp": (now - datetime.timedelta(minutes=10)).isoformat(),
|
|
28
|
+
"location": None,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "error",
|
|
32
|
+
"text": "JavaScript error occurred",
|
|
33
|
+
"timestamp": (now - datetime.timedelta(minutes=8)).isoformat(),
|
|
34
|
+
"location": {"url": "test.js", "lineNumber": 42, "columnNumber": 10},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "warning",
|
|
38
|
+
"text": "Deprecated API usage",
|
|
39
|
+
"timestamp": (now - datetime.timedelta(minutes=6)).isoformat(),
|
|
40
|
+
"location": None,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"type": "log",
|
|
44
|
+
"text": "Regular log message 2",
|
|
45
|
+
"timestamp": (now - datetime.timedelta(minutes=4)).isoformat(),
|
|
46
|
+
"location": None,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"type": "assert",
|
|
50
|
+
"text": "Assertion failed: condition not met",
|
|
51
|
+
"timestamp": (now - datetime.timedelta(minutes=2)).isoformat(),
|
|
52
|
+
"location": None,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "info",
|
|
56
|
+
"text": "Information message",
|
|
57
|
+
"timestamp": now.isoformat(),
|
|
58
|
+
"location": None,
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"type": "chromium",
|
|
64
|
+
"url": "https://example.com",
|
|
65
|
+
"console_logs": console_logs,
|
|
66
|
+
"launched_at": now.isoformat(),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_get_browser_console_logs_default_filtering(self, browser_manager, mock_browser_info):
|
|
71
|
+
"""Test default console log filtering (errors, warnings, assertions only)."""
|
|
72
|
+
browser_id = "test-browser-id"
|
|
73
|
+
browser_manager.browsers[browser_id] = mock_browser_info
|
|
74
|
+
|
|
75
|
+
result = await browser_manager.get_browser_console_logs(browser_id)
|
|
76
|
+
|
|
77
|
+
assert result["total_logs_count"] == 6
|
|
78
|
+
assert result["returned_count"] == 3 # Only error, warning, assert
|
|
79
|
+
assert result["filters_applied"]["log_types"] == ["error", "warning", "assert"]
|
|
80
|
+
assert result["filters_applied"]["max_logs"] == 50
|
|
81
|
+
assert result["filters_applied"]["max_chars"] == 8000
|
|
82
|
+
|
|
83
|
+
# Check that only the correct log types are returned
|
|
84
|
+
returned_types = [log["type"] for log in result["console_logs"]]
|
|
85
|
+
assert "error" in returned_types
|
|
86
|
+
assert "warning" in returned_types
|
|
87
|
+
assert "assert" in returned_types
|
|
88
|
+
assert "log" not in returned_types
|
|
89
|
+
assert "info" not in returned_types
|
|
90
|
+
|
|
91
|
+
@pytest.mark.asyncio
|
|
92
|
+
async def test_get_browser_console_logs_custom_log_types(self, browser_manager, mock_browser_info):
|
|
93
|
+
"""Test filtering by custom log types."""
|
|
94
|
+
browser_id = "test-browser-id"
|
|
95
|
+
browser_manager.browsers[browser_id] = mock_browser_info
|
|
96
|
+
|
|
97
|
+
result = await browser_manager.get_browser_console_logs(browser_id, log_types=["log", "info"])
|
|
98
|
+
|
|
99
|
+
assert result["total_logs_count"] == 6
|
|
100
|
+
assert result["returned_count"] == 3 # 2 log + 1 info
|
|
101
|
+
assert result["filters_applied"]["log_types"] == ["log", "info"]
|
|
102
|
+
|
|
103
|
+
# Check that only the correct log types are returned
|
|
104
|
+
returned_types = [log["type"] for log in result["console_logs"]]
|
|
105
|
+
assert "log" in returned_types
|
|
106
|
+
assert "info" in returned_types
|
|
107
|
+
assert "error" not in returned_types
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_get_browser_console_logs_max_logs_limit(self, browser_manager, mock_browser_info):
|
|
111
|
+
"""Test limiting the number of logs returned."""
|
|
112
|
+
browser_id = "test-browser-id"
|
|
113
|
+
browser_manager.browsers[browser_id] = mock_browser_info
|
|
114
|
+
|
|
115
|
+
result = await browser_manager.get_browser_console_logs(
|
|
116
|
+
browser_id, max_logs=2, log_types=[] # Empty list to include all types
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert result["total_logs_count"] == 6
|
|
120
|
+
assert result["returned_count"] == 2 # Limited to 2 most recent
|
|
121
|
+
assert result["filters_applied"]["max_logs"] == 2
|
|
122
|
+
|
|
123
|
+
# Should return the 2 most recent logs
|
|
124
|
+
returned_logs = result["console_logs"]
|
|
125
|
+
assert len(returned_logs) == 2
|
|
126
|
+
assert returned_logs[-1]["type"] == "info" # Most recent
|
|
127
|
+
assert returned_logs[-2]["type"] == "assert" # Second most recent
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_get_browser_console_logs_time_filtering(self, browser_manager, mock_browser_info):
|
|
131
|
+
"""Test filtering logs by time window."""
|
|
132
|
+
browser_id = "test-browser-id"
|
|
133
|
+
browser_manager.browsers[browser_id] = mock_browser_info
|
|
134
|
+
|
|
135
|
+
result = await browser_manager.get_browser_console_logs(
|
|
136
|
+
browser_id, minutes_back=5, log_types=[] # Include all types, filter by time
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
assert result["total_logs_count"] == 6
|
|
140
|
+
assert result["returned_count"] == 3 # Only logs from last 5 minutes
|
|
141
|
+
assert result["filters_applied"]["minutes_back"] == 5
|
|
142
|
+
|
|
143
|
+
# All returned logs should be within the time window
|
|
144
|
+
cutoff_time = datetime.datetime.now() - datetime.timedelta(minutes=5)
|
|
145
|
+
for log in result["console_logs"]:
|
|
146
|
+
log_time = datetime.datetime.fromisoformat(log["timestamp"])
|
|
147
|
+
assert log_time > cutoff_time
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def test_get_browser_console_logs_character_limit(self, browser_manager, mock_browser_info):
|
|
151
|
+
"""Test limiting logs by character count."""
|
|
152
|
+
browser_id = "test-browser-id"
|
|
153
|
+
browser_manager.browsers[browser_id] = mock_browser_info
|
|
154
|
+
|
|
155
|
+
# Set a very low character limit to test truncation
|
|
156
|
+
result = await browser_manager.get_browser_console_logs(
|
|
157
|
+
browser_id, max_chars=50, log_types=[] # Include all types
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert result["total_logs_count"] == 6
|
|
161
|
+
assert result["returned_count"] <= 6 # Should be limited by character count
|
|
162
|
+
|
|
163
|
+
# Calculate total character count of returned logs
|
|
164
|
+
total_chars = sum(len(f"{log['type']}: {log['text']}") for log in result["console_logs"])
|
|
165
|
+
assert total_chars <= 50
|
|
166
|
+
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
async def test_get_browser_console_logs_combined_filters(self, browser_manager, mock_browser_info):
|
|
169
|
+
"""Test applying multiple filters simultaneously."""
|
|
170
|
+
browser_id = "test-browser-id"
|
|
171
|
+
browser_manager.browsers[browser_id] = mock_browser_info
|
|
172
|
+
|
|
173
|
+
result = await browser_manager.get_browser_console_logs(
|
|
174
|
+
browser_id, max_logs=10, log_types=["error", "warning"], minutes_back=10, max_chars=1000
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
assert result["total_logs_count"] == 6
|
|
178
|
+
assert result["filters_applied"]["log_types"] == ["error", "warning"]
|
|
179
|
+
assert result["filters_applied"]["minutes_back"] == 10
|
|
180
|
+
assert result["filters_applied"]["max_logs"] == 10
|
|
181
|
+
assert result["filters_applied"]["max_chars"] == 1000
|
|
182
|
+
|
|
183
|
+
# Should only contain error and warning logs
|
|
184
|
+
returned_types = [log["type"] for log in result["console_logs"]]
|
|
185
|
+
for log_type in returned_types:
|
|
186
|
+
assert log_type in ["error", "warning"]
|
|
187
|
+
|
|
188
|
+
@pytest.mark.asyncio
|
|
189
|
+
async def test_get_browser_console_logs_empty_logs(self, browser_manager):
|
|
190
|
+
"""Test behavior when no console logs exist."""
|
|
191
|
+
browser_id = "test-browser-id"
|
|
192
|
+
browser_manager.browsers[browser_id] = {
|
|
193
|
+
"console_logs": [],
|
|
194
|
+
"launched_at": datetime.datetime.now().isoformat(),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
result = await browser_manager.get_browser_console_logs(browser_id)
|
|
198
|
+
|
|
199
|
+
assert result["total_logs_count"] == 0
|
|
200
|
+
assert result["returned_count"] == 0
|
|
201
|
+
assert result["console_logs"] == []
|
|
202
|
+
|
|
203
|
+
@pytest.mark.asyncio
|
|
204
|
+
async def test_get_browser_console_logs_browser_not_found(self, browser_manager):
|
|
205
|
+
"""Test error handling when browser ID doesn't exist."""
|
|
206
|
+
with pytest.raises(KeyError, match="Browser with ID nonexistent not found"):
|
|
207
|
+
await browser_manager.get_browser_console_logs("nonexistent")
|
|
208
|
+
|
|
209
|
+
@pytest.mark.asyncio
|
|
210
|
+
async def test_get_browser_content_with_filtered_logs(self, browser_manager, mock_browser_info):
|
|
211
|
+
"""Test that get_browser_content uses filtered console logs."""
|
|
212
|
+
browser_id = "test-browser-id"
|
|
213
|
+
|
|
214
|
+
# Mock the page object
|
|
215
|
+
mock_page = AsyncMock()
|
|
216
|
+
mock_page.url = "https://example.com"
|
|
217
|
+
mock_page.title.return_value = "Test Page"
|
|
218
|
+
mock_page.content.return_value = "<html><body>Test</body></html>"
|
|
219
|
+
|
|
220
|
+
mock_browser_info["page"] = mock_page
|
|
221
|
+
browser_manager.browsers[browser_id] = mock_browser_info
|
|
222
|
+
|
|
223
|
+
result = await browser_manager.get_browser_content(browser_id, max_logs=2, log_types=["error"])
|
|
224
|
+
|
|
225
|
+
assert "current_url" in result
|
|
226
|
+
assert "title" in result
|
|
227
|
+
assert "html" in result
|
|
228
|
+
assert "console_logs" in result
|
|
229
|
+
assert "console_log_metadata" in result
|
|
230
|
+
|
|
231
|
+
metadata = result["console_log_metadata"]
|
|
232
|
+
assert metadata["total_logs_count"] == 6
|
|
233
|
+
assert metadata["returned_count"] == 1 # Only 1 error log
|
|
234
|
+
assert metadata["filters_applied"]["log_types"] == ["error"]
|
|
235
|
+
assert metadata["filters_applied"]["max_logs"] == 2
|
|
236
|
+
|
|
237
|
+
def test_circular_buffer_implementation(self, browser_manager):
|
|
238
|
+
"""Test that the circular buffer prevents unlimited log growth."""
|
|
239
|
+
# Set a small buffer size for testing
|
|
240
|
+
browser_manager.max_console_logs_per_browser = 3
|
|
241
|
+
|
|
242
|
+
console_logs = []
|
|
243
|
+
|
|
244
|
+
# Simulate the console log handler behavior
|
|
245
|
+
def simulate_console_log_handler(msg_text, msg_type="log"):
|
|
246
|
+
log_entry = {
|
|
247
|
+
"type": msg_type,
|
|
248
|
+
"text": msg_text,
|
|
249
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
250
|
+
"location": None,
|
|
251
|
+
}
|
|
252
|
+
console_logs.append(log_entry)
|
|
253
|
+
|
|
254
|
+
# Implement circular buffer logic
|
|
255
|
+
if len(console_logs) > browser_manager.max_console_logs_per_browser:
|
|
256
|
+
console_logs.pop(0) # Remove oldest log
|
|
257
|
+
|
|
258
|
+
# Add more logs than the buffer size
|
|
259
|
+
simulate_console_log_handler("Log 1")
|
|
260
|
+
simulate_console_log_handler("Log 2")
|
|
261
|
+
simulate_console_log_handler("Log 3")
|
|
262
|
+
assert len(console_logs) == 3
|
|
263
|
+
|
|
264
|
+
simulate_console_log_handler("Log 4")
|
|
265
|
+
assert len(console_logs) == 3 # Should still be 3
|
|
266
|
+
assert console_logs[0]["text"] == "Log 2" # First log should be removed
|
|
267
|
+
assert console_logs[-1]["text"] == "Log 4" # Last log should be the newest
|
|
268
|
+
|
|
269
|
+
simulate_console_log_handler("Log 5")
|
|
270
|
+
assert len(console_logs) == 3
|
|
271
|
+
assert console_logs[0]["text"] == "Log 3"
|
|
272
|
+
assert console_logs[-1]["text"] == "Log 5"
|
|
273
|
+
|
|
274
|
+
@pytest.mark.asyncio
|
|
275
|
+
async def test_no_log_types_filter_includes_all(self, browser_manager, mock_browser_info):
|
|
276
|
+
"""Test that passing an empty list for log_types includes all log types."""
|
|
277
|
+
browser_id = "test-browser-id"
|
|
278
|
+
browser_manager.browsers[browser_id] = mock_browser_info
|
|
279
|
+
|
|
280
|
+
result = await browser_manager.get_browser_console_logs(
|
|
281
|
+
browser_id, log_types=[] # Empty list should include all types
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
assert result["total_logs_count"] == 6
|
|
285
|
+
assert result["returned_count"] == 6 # All logs should be included
|
|
286
|
+
assert result["filters_applied"]["log_types"] == []
|
|
287
|
+
|
|
288
|
+
# Should include all log types
|
|
289
|
+
returned_types = [log["type"] for log in result["console_logs"]]
|
|
290
|
+
assert "log" in returned_types
|
|
291
|
+
assert "error" in returned_types
|
|
292
|
+
assert "warning" in returned_types
|
|
293
|
+
assert "assert" in returned_types
|
|
294
|
+
assert "info" in returned_types
|
|
295
|
+
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
async def test_character_limit_preserves_most_recent(self, browser_manager):
|
|
298
|
+
"""Test that character limit preserves the most recent logs."""
|
|
299
|
+
browser_id = "test-browser-id"
|
|
300
|
+
|
|
301
|
+
# Create logs with known character counts
|
|
302
|
+
console_logs = [
|
|
303
|
+
{
|
|
304
|
+
"type": "log",
|
|
305
|
+
"text": "A" * 10, # 10 chars + "log: " = 14 chars
|
|
306
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
307
|
+
"location": None,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"type": "log",
|
|
311
|
+
"text": "B" * 10, # 10 chars + "log: " = 14 chars
|
|
312
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
313
|
+
"location": None,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"type": "log",
|
|
317
|
+
"text": "C" * 10, # 10 chars + "log: " = 14 chars (most recent)
|
|
318
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
319
|
+
"location": None,
|
|
320
|
+
},
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
browser_manager.browsers[browser_id] = {
|
|
324
|
+
"console_logs": console_logs,
|
|
325
|
+
"launched_at": datetime.datetime.now().isoformat(),
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
# Set character limit to allow only 1 log (14 chars) to test the logic
|
|
329
|
+
result = await browser_manager.get_browser_console_logs(browser_id, max_chars=14, log_types=[])
|
|
330
|
+
|
|
331
|
+
assert result["returned_count"] == 1
|
|
332
|
+
# Should preserve the most recent log (C)
|
|
333
|
+
returned_texts = [log["text"] for log in result["console_logs"]]
|
|
334
|
+
assert "C" * 10 in returned_texts
|
|
335
|
+
assert "A" * 10 not in returned_texts
|
|
336
|
+
assert "B" * 10 not in returned_texts
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class TestPlaywrightBrowserManagerIntegration:
|
|
340
|
+
"""Integration tests for PlaywrightBrowserManager that use real browsers."""
|
|
341
|
+
|
|
342
|
+
@pytest.mark.integration
|
|
343
|
+
@pytest.mark.skipif(SKIP_IN_CI, reason="Skipping network-dependent test in CI environment")
|
|
344
|
+
@pytest.mark.asyncio
|
|
345
|
+
async def test_real_browser_google_screenshot(self):
|
|
346
|
+
"""Integration test: Launch real browser, load Google, take screenshot."""
|
|
347
|
+
# Check if BROWSERLESS_API_KEY is available, skip if not
|
|
348
|
+
if not os.getenv("BROWSERLESS_API_KEY"):
|
|
349
|
+
pytest.skip("BROWSERLESS_API_KEY environment variable not set")
|
|
350
|
+
|
|
351
|
+
browser_manager = PlaywrightBrowserManager(browser_backend="browserless")
|
|
352
|
+
browser_id = None
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Launch browser and navigate to Google
|
|
356
|
+
browser_id = await browser_manager.launch_browser("https://www.google.com")
|
|
357
|
+
|
|
358
|
+
# Verify we got a valid browser ID (not an error dict)
|
|
359
|
+
assert isinstance(browser_id, str)
|
|
360
|
+
assert browser_id != ""
|
|
361
|
+
|
|
362
|
+
# Verify browser is in the manager's registry
|
|
363
|
+
assert browser_id in browser_manager.browsers
|
|
364
|
+
browser_info = browser_manager.browsers[browser_id]
|
|
365
|
+
assert browser_info["url"] == "https://www.google.com"
|
|
366
|
+
assert browser_info["backend"] == "browserless"
|
|
367
|
+
assert browser_info["browserstack"] is False
|
|
368
|
+
|
|
369
|
+
# Take a screenshot
|
|
370
|
+
screenshot_result = await browser_manager.take_browser_screenshot(browser_id)
|
|
371
|
+
|
|
372
|
+
# Verify screenshot result structure
|
|
373
|
+
assert "current_url" in screenshot_result
|
|
374
|
+
assert "title" in screenshot_result
|
|
375
|
+
assert "screenshot" in screenshot_result
|
|
376
|
+
|
|
377
|
+
# Verify we're actually on Google
|
|
378
|
+
assert "google" in screenshot_result["current_url"].lower()
|
|
379
|
+
assert "google" in screenshot_result["title"].lower()
|
|
380
|
+
|
|
381
|
+
# Verify screenshot is base64 encoded
|
|
382
|
+
screenshot_data = screenshot_result["screenshot"]
|
|
383
|
+
assert isinstance(screenshot_data, str)
|
|
384
|
+
assert len(screenshot_data) > 0
|
|
385
|
+
# Basic check that it's base64 (starts with image header)
|
|
386
|
+
import base64
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
decoded = base64.b64decode(screenshot_data)
|
|
390
|
+
assert len(decoded) > 0
|
|
391
|
+
except Exception:
|
|
392
|
+
pytest.fail("Screenshot is not valid base64 data")
|
|
393
|
+
|
|
394
|
+
# Test console logs capture
|
|
395
|
+
console_logs_result = await browser_manager.get_browser_console_logs(browser_id)
|
|
396
|
+
assert "console_logs" in console_logs_result
|
|
397
|
+
assert "total_logs_count" in console_logs_result
|
|
398
|
+
assert "returned_count" in console_logs_result
|
|
399
|
+
|
|
400
|
+
# Test browser content retrieval
|
|
401
|
+
content_result = await browser_manager.get_browser_content(browser_id)
|
|
402
|
+
assert "current_url" in content_result
|
|
403
|
+
assert "title" in content_result
|
|
404
|
+
assert "html" in content_result
|
|
405
|
+
assert "console_logs" in content_result
|
|
406
|
+
assert len(content_result["html"]) > 0
|
|
407
|
+
|
|
408
|
+
# Test interactive elements extraction
|
|
409
|
+
elements_result = await browser_manager.get_browser_interactive_elements(browser_id)
|
|
410
|
+
assert "current_url" in elements_result
|
|
411
|
+
assert "title" in elements_result
|
|
412
|
+
assert "interactive_elements" in elements_result
|
|
413
|
+
|
|
414
|
+
finally:
|
|
415
|
+
# Clean up: close the browser if it was created
|
|
416
|
+
if browser_id and browser_id in browser_manager.browsers:
|
|
417
|
+
await browser_manager.close_browser(browser_id)
|
|
418
|
+
|
|
419
|
+
# Additional cleanup to ensure all browsers are closed
|
|
420
|
+
await browser_manager.cleanup_all_browsers()
|
|
421
|
+
|
|
422
|
+
@pytest.mark.integration
|
|
423
|
+
@pytest.mark.asyncio
|
|
424
|
+
@pytest.mark.skipif(SKIP_IN_CI, reason="Skipping slow test in CI environment")
|
|
425
|
+
async def test_real_browser_error_handling(self):
|
|
426
|
+
"""Integration test: Test error handling with real browser for invalid URLs."""
|
|
427
|
+
# Check if BROWSERLESS_API_KEY is available, skip if not
|
|
428
|
+
if not os.getenv("BROWSERLESS_API_KEY"):
|
|
429
|
+
pytest.skip("BROWSERLESS_API_KEY environment variable not set")
|
|
430
|
+
|
|
431
|
+
browser_manager = PlaywrightBrowserManager(browser_backend="browserless")
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
# Try to launch browser with invalid URL
|
|
435
|
+
result = await browser_manager.launch_browser("not-a-valid-url")
|
|
436
|
+
|
|
437
|
+
# Should return error dict rather than browser ID
|
|
438
|
+
if isinstance(result, dict) and "error" in result:
|
|
439
|
+
assert "error" in result
|
|
440
|
+
assert "Browser Launch Error" in result["error"]
|
|
441
|
+
else:
|
|
442
|
+
# If it somehow succeeds (maybe Playwright handles it), clean up
|
|
443
|
+
if isinstance(result, str) and result in browser_manager.browsers:
|
|
444
|
+
await browser_manager.close_browser(result)
|
|
445
|
+
|
|
446
|
+
finally:
|
|
447
|
+
await browser_manager.cleanup_all_browsers()
|