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,335 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import pytest
|
|
3
|
+
import uuid
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
5
|
+
from kolega_code.agent.tool_backend.browser_tool import BrowserTool
|
|
6
|
+
from kolega_code.config import AgentConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestBrowserTool:
|
|
10
|
+
"""Test suite for BrowserTool console log filtering functionality."""
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_config(self):
|
|
14
|
+
"""Create a mock agent config."""
|
|
15
|
+
return MagicMock(spec=AgentConfig)
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_connection_manager(self):
|
|
19
|
+
"""Create a mock connection manager."""
|
|
20
|
+
return AsyncMock()
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_caller(self):
|
|
24
|
+
"""Create a mock caller."""
|
|
25
|
+
caller = MagicMock()
|
|
26
|
+
caller.agent_name = "test-agent"
|
|
27
|
+
return caller
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def browser_tool(self, mock_config, mock_connection_manager, mock_caller):
|
|
31
|
+
"""Create a browser tool instance for testing."""
|
|
32
|
+
return BrowserTool(
|
|
33
|
+
project_path="/test/path",
|
|
34
|
+
workspace_id="test-workspace",
|
|
35
|
+
thread_id=str(uuid.uuid4()),
|
|
36
|
+
connection_manager=mock_connection_manager,
|
|
37
|
+
config=mock_config,
|
|
38
|
+
caller=mock_caller,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def mock_console_logs_result(self):
|
|
43
|
+
"""Create mock console logs result from browser manager."""
|
|
44
|
+
return {
|
|
45
|
+
"console_logs": [
|
|
46
|
+
{
|
|
47
|
+
"type": "error",
|
|
48
|
+
"text": "JavaScript error occurred",
|
|
49
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
50
|
+
"location": {"url": "test.js", "lineNumber": 42, "columnNumber": 10},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"type": "warning",
|
|
54
|
+
"text": "Deprecated API usage",
|
|
55
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
56
|
+
"location": None,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
"total_logs_count": 10,
|
|
60
|
+
"returned_count": 2,
|
|
61
|
+
"filters_applied": {
|
|
62
|
+
"max_logs": 50,
|
|
63
|
+
"log_types": ["error", "warning", "assert"],
|
|
64
|
+
"minutes_back": None,
|
|
65
|
+
"max_chars": 8000,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_get_browser_console_logs_default_parameters(self, browser_tool, mock_console_logs_result):
|
|
71
|
+
"""Test get_browser_console_logs with default parameters."""
|
|
72
|
+
browser_tool.browser_manager.get_browser_console_logs = AsyncMock(return_value=mock_console_logs_result)
|
|
73
|
+
|
|
74
|
+
result = await browser_tool.get_browser_console_logs("test-browser-id")
|
|
75
|
+
|
|
76
|
+
# Verify the browser manager was called with default parameters
|
|
77
|
+
browser_tool.browser_manager.get_browser_console_logs.assert_called_once_with(
|
|
78
|
+
"test-browser-id", max_logs=50, log_types=None, minutes_back=None, max_chars=8000
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Check that the result contains expected markdown formatting
|
|
82
|
+
assert "## Console Logs" in result
|
|
83
|
+
assert "**Showing 2 of 10 total logs**" in result
|
|
84
|
+
assert "**Filtered by types:** error, warning, assert" in result
|
|
85
|
+
assert "**Max logs:** 50" in result
|
|
86
|
+
assert "| Type | Timestamp | Message | Location |" in result
|
|
87
|
+
assert "| error |" in result
|
|
88
|
+
assert "| warning |" in result
|
|
89
|
+
assert "JavaScript error occurred" in result
|
|
90
|
+
assert "Deprecated API usage" in result
|
|
91
|
+
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_get_browser_console_logs_custom_parameters(self, browser_tool, mock_console_logs_result):
|
|
94
|
+
"""Test get_browser_console_logs with custom parameters."""
|
|
95
|
+
# Update mock result to reflect custom parameters
|
|
96
|
+
mock_console_logs_result["filters_applied"] = {
|
|
97
|
+
"max_logs": 10,
|
|
98
|
+
"log_types": ["error"],
|
|
99
|
+
"minutes_back": 5,
|
|
100
|
+
"max_chars": 1000,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
browser_tool.browser_manager.get_browser_console_logs = AsyncMock(return_value=mock_console_logs_result)
|
|
104
|
+
|
|
105
|
+
result = await browser_tool.get_browser_console_logs(
|
|
106
|
+
"test-browser-id", max_logs=10, log_types=["error"], minutes_back=5, max_chars=1000
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Verify the browser manager was called with custom parameters
|
|
110
|
+
browser_tool.browser_manager.get_browser_console_logs.assert_called_once_with(
|
|
111
|
+
"test-browser-id", max_logs=10, log_types=["error"], minutes_back=5, max_chars=1000
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Check that the result reflects custom filtering
|
|
115
|
+
assert "**Filtered by types:** error" in result
|
|
116
|
+
assert "**Time window:** Last 5 minutes" in result
|
|
117
|
+
assert "**Character limit:** 1000" in result
|
|
118
|
+
assert "**Max logs:** 10" in result
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_get_browser_console_logs_no_logs(self, browser_tool):
|
|
122
|
+
"""Test get_browser_console_logs when no logs are found."""
|
|
123
|
+
mock_empty_result = {
|
|
124
|
+
"console_logs": [],
|
|
125
|
+
"total_logs_count": 0,
|
|
126
|
+
"returned_count": 0,
|
|
127
|
+
"filters_applied": {
|
|
128
|
+
"max_logs": 50,
|
|
129
|
+
"log_types": ["error", "warning", "assert"],
|
|
130
|
+
"minutes_back": None,
|
|
131
|
+
"max_chars": 8000,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
browser_tool.browser_manager.get_browser_console_logs = AsyncMock(return_value=mock_empty_result)
|
|
136
|
+
|
|
137
|
+
result = await browser_tool.get_browser_console_logs("test-browser-id")
|
|
138
|
+
|
|
139
|
+
assert result == "## Console Logs\n\nNo console logs found."
|
|
140
|
+
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_get_browser_console_logs_location_formatting(self, browser_tool):
|
|
143
|
+
"""Test that console log locations are formatted correctly."""
|
|
144
|
+
mock_result = {
|
|
145
|
+
"console_logs": [
|
|
146
|
+
{
|
|
147
|
+
"type": "error",
|
|
148
|
+
"text": "Error with location",
|
|
149
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
150
|
+
"location": {"url": "test.js", "lineNumber": 42, "columnNumber": 10},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"type": "warning",
|
|
154
|
+
"text": "Warning without location",
|
|
155
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
156
|
+
"location": None,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
"total_logs_count": 2,
|
|
160
|
+
"returned_count": 2,
|
|
161
|
+
"filters_applied": {
|
|
162
|
+
"max_logs": 50,
|
|
163
|
+
"log_types": ["error", "warning", "assert"],
|
|
164
|
+
"minutes_back": None,
|
|
165
|
+
"max_chars": 8000,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
browser_tool.browser_manager.get_browser_console_logs = AsyncMock(return_value=mock_result)
|
|
170
|
+
|
|
171
|
+
result = await browser_tool.get_browser_console_logs("test-browser-id")
|
|
172
|
+
|
|
173
|
+
# Check location formatting
|
|
174
|
+
assert "test.js:42:10" in result # Formatted location
|
|
175
|
+
assert "N/A" in result # No location case
|
|
176
|
+
|
|
177
|
+
@pytest.mark.asyncio
|
|
178
|
+
async def test_get_browser_console_logs_escapes_pipe_characters(self, browser_tool):
|
|
179
|
+
"""Test that pipe characters in log messages are escaped for markdown tables."""
|
|
180
|
+
mock_result = {
|
|
181
|
+
"console_logs": [
|
|
182
|
+
{
|
|
183
|
+
"type": "error",
|
|
184
|
+
"text": "Error with | pipe character",
|
|
185
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
186
|
+
"location": None,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
"total_logs_count": 1,
|
|
190
|
+
"returned_count": 1,
|
|
191
|
+
"filters_applied": {
|
|
192
|
+
"max_logs": 50,
|
|
193
|
+
"log_types": ["error", "warning", "assert"],
|
|
194
|
+
"minutes_back": None,
|
|
195
|
+
"max_chars": 8000,
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
browser_tool.browser_manager.get_browser_console_logs = AsyncMock(return_value=mock_result)
|
|
200
|
+
|
|
201
|
+
result = await browser_tool.get_browser_console_logs("test-browser-id")
|
|
202
|
+
|
|
203
|
+
# Check that pipe character is escaped
|
|
204
|
+
assert "Error with \\| pipe character" in result
|
|
205
|
+
|
|
206
|
+
@pytest.mark.asyncio
|
|
207
|
+
async def test_get_browser_content_with_console_log_filtering(self, browser_tool):
|
|
208
|
+
"""Test get_browser_content with console log filtering."""
|
|
209
|
+
mock_content_result = {
|
|
210
|
+
"current_url": "https://example.com",
|
|
211
|
+
"title": "Test Page",
|
|
212
|
+
"html": "<html><body>Test</body></html>",
|
|
213
|
+
"console_logs": [
|
|
214
|
+
{
|
|
215
|
+
"type": "error",
|
|
216
|
+
"text": "JavaScript error",
|
|
217
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
218
|
+
"location": None,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
"console_log_metadata": {
|
|
222
|
+
"total_logs_count": 5,
|
|
223
|
+
"returned_count": 1,
|
|
224
|
+
"filters_applied": {"max_logs": 10, "log_types": ["error"], "minutes_back": None, "max_chars": 1000},
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
browser_tool.browser_manager.get_browser_content = AsyncMock(return_value=mock_content_result)
|
|
229
|
+
|
|
230
|
+
result = await browser_tool.get_browser_content(
|
|
231
|
+
"test-browser-id", max_logs=10, log_types=["error"], max_chars=1000
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Verify the browser manager was called with filtering parameters
|
|
235
|
+
browser_tool.browser_manager.get_browser_content.assert_called_once_with(
|
|
236
|
+
"test-browser-id", max_logs=10, log_types=["error"], minutes_back=None, max_chars=1000
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Check that the result contains expected content
|
|
240
|
+
assert "# Browser Content: Test Page" in result
|
|
241
|
+
assert "**Current URL:** https://example.com" in result
|
|
242
|
+
assert "## Console Logs" in result
|
|
243
|
+
assert "**Showing 1 of 5 total logs**" in result
|
|
244
|
+
assert "**Filtered by types:** error" in result
|
|
245
|
+
assert "**Character limit:** 1000" in result
|
|
246
|
+
assert "**Max logs:** 10" in result
|
|
247
|
+
assert "## Page HTML" in result
|
|
248
|
+
assert "<html><body>Test</body></html>" in result
|
|
249
|
+
|
|
250
|
+
@pytest.mark.asyncio
|
|
251
|
+
async def test_get_browser_content_no_console_logs(self, browser_tool):
|
|
252
|
+
"""Test get_browser_content when there are no console logs."""
|
|
253
|
+
mock_content_result = {
|
|
254
|
+
"current_url": "https://example.com",
|
|
255
|
+
"title": "Test Page",
|
|
256
|
+
"html": "<html><body>Test</body></html>",
|
|
257
|
+
"console_logs": [],
|
|
258
|
+
"console_log_metadata": {
|
|
259
|
+
"total_logs_count": 0,
|
|
260
|
+
"returned_count": 0,
|
|
261
|
+
"filters_applied": {
|
|
262
|
+
"max_logs": 50,
|
|
263
|
+
"log_types": ["error", "warning", "assert"],
|
|
264
|
+
"minutes_back": None,
|
|
265
|
+
"max_chars": 8000,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
browser_tool.browser_manager.get_browser_content = AsyncMock(return_value=mock_content_result)
|
|
271
|
+
|
|
272
|
+
result = await browser_tool.get_browser_content("test-browser-id")
|
|
273
|
+
|
|
274
|
+
# Should not contain console logs section when there are no logs
|
|
275
|
+
assert "# Browser Content: Test Page" in result
|
|
276
|
+
assert "**Current URL:** https://example.com" in result
|
|
277
|
+
assert "## Console Logs" not in result
|
|
278
|
+
assert "## Page HTML" in result
|
|
279
|
+
|
|
280
|
+
@pytest.mark.asyncio
|
|
281
|
+
async def test_get_browser_content_truncates_large_html(self, browser_tool):
|
|
282
|
+
mock_content_result = {
|
|
283
|
+
"current_url": "https://example.com",
|
|
284
|
+
"title": "Large Page",
|
|
285
|
+
"html": "a" * 100_050,
|
|
286
|
+
"console_logs": [],
|
|
287
|
+
"console_log_metadata": {
|
|
288
|
+
"total_logs_count": 0,
|
|
289
|
+
"returned_count": 0,
|
|
290
|
+
"filters_applied": {
|
|
291
|
+
"max_logs": 50,
|
|
292
|
+
"log_types": ["error", "warning", "assert"],
|
|
293
|
+
"minutes_back": None,
|
|
294
|
+
"max_chars": 8000,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
browser_tool.browser_manager.get_browser_content = AsyncMock(return_value=mock_content_result)
|
|
300
|
+
|
|
301
|
+
result = await browser_tool.get_browser_content("test-browser-id")
|
|
302
|
+
|
|
303
|
+
assert "HTML truncated by size: Showing first 100,000 of 100,050 characters" in result
|
|
304
|
+
html_content = result.split("```html\n", 1)[1].rsplit("\n```", 1)[0]
|
|
305
|
+
assert len(html_content) == 100_000
|
|
306
|
+
|
|
307
|
+
@pytest.mark.asyncio
|
|
308
|
+
async def test_get_browser_content_without_metadata(self, browser_tool):
|
|
309
|
+
"""Test get_browser_content when console_log_metadata is missing."""
|
|
310
|
+
mock_content_result = {
|
|
311
|
+
"current_url": "https://example.com",
|
|
312
|
+
"title": "Test Page",
|
|
313
|
+
"html": "<html><body>Test</body></html>",
|
|
314
|
+
"console_logs": [
|
|
315
|
+
{
|
|
316
|
+
"type": "error",
|
|
317
|
+
"text": "JavaScript error",
|
|
318
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
319
|
+
"location": None,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
# Missing console_log_metadata
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
browser_tool.browser_manager.get_browser_content = AsyncMock(return_value=mock_content_result)
|
|
326
|
+
|
|
327
|
+
result = await browser_tool.get_browser_content("test-browser-id")
|
|
328
|
+
|
|
329
|
+
# Should still work without metadata
|
|
330
|
+
assert "# Browser Content: Test Page" in result
|
|
331
|
+
assert "## Console Logs" in result
|
|
332
|
+
assert "JavaScript error" in result
|
|
333
|
+
# Should not contain metadata information
|
|
334
|
+
assert "**Showing" not in result
|
|
335
|
+
assert "**Filtered by types:**" not in result
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from kolega_code.agent.tool_backend.build_tool import BuildTool
|
|
7
|
+
from kolega_code.agent.tool_backend.base_tool import BaseTool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DummyFS:
|
|
11
|
+
def __init__(self, files: dict[str, str]):
|
|
12
|
+
self._files = files
|
|
13
|
+
|
|
14
|
+
def exists(self, path: str) -> bool:
|
|
15
|
+
return path in self._files
|
|
16
|
+
|
|
17
|
+
def read_text(self, path: str, encoding: str = "utf-8") -> str:
|
|
18
|
+
if path not in self._files:
|
|
19
|
+
raise FileNotFoundError(path)
|
|
20
|
+
return self._files[path]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DummyTM:
|
|
24
|
+
def __init__(self, outputs: dict[str, str] | None = None):
|
|
25
|
+
self.outputs = outputs or {}
|
|
26
|
+
self.calls = []
|
|
27
|
+
|
|
28
|
+
async def run_command(self, command: str, cwd: str | None = None, timeout: int | None = None) -> str:
|
|
29
|
+
self.calls.append((command, cwd, timeout))
|
|
30
|
+
return self.outputs.get(command, f"ok: {command}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def make_tool(fs_map: dict[str, str], tm_outputs: dict[str, str] | None = None) -> BuildTool:
|
|
34
|
+
tool = BuildTool(
|
|
35
|
+
project_path=Path("/repo"),
|
|
36
|
+
workspace_id="ws",
|
|
37
|
+
thread_id="th",
|
|
38
|
+
connection_manager=None,
|
|
39
|
+
config=None,
|
|
40
|
+
caller=None,
|
|
41
|
+
filesystem=DummyFS(fs_map),
|
|
42
|
+
terminal_manager=DummyTM(tm_outputs),
|
|
43
|
+
)
|
|
44
|
+
return tool
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_build_backend_specific_command():
|
|
49
|
+
manifest = """
|
|
50
|
+
name: demo
|
|
51
|
+
runtime: node:18
|
|
52
|
+
backend_build_command: npm run build:api
|
|
53
|
+
"""
|
|
54
|
+
tool = make_tool({".kolega-manifest.yaml": manifest})
|
|
55
|
+
result = await tool.build_backend()
|
|
56
|
+
assert "npm run build:api" in result
|
|
57
|
+
assert "ok: npm run build:api" in result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_build_frontend_specific_command():
|
|
62
|
+
manifest = """
|
|
63
|
+
name: demo
|
|
64
|
+
runtime: node:18
|
|
65
|
+
frontend_build_command: npm run build:web
|
|
66
|
+
"""
|
|
67
|
+
tool = make_tool({".kolega-manifest.yaml": manifest})
|
|
68
|
+
result = await tool.build_frontend()
|
|
69
|
+
assert "npm run build:web" in result
|
|
70
|
+
assert "ok: npm run build:web" in result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_build_fallback_to_generic_build_command():
|
|
75
|
+
manifest = """
|
|
76
|
+
name: demo
|
|
77
|
+
runtime: node:18
|
|
78
|
+
build_command: npm run build
|
|
79
|
+
"""
|
|
80
|
+
tool = make_tool({".kolega-manifest.yaml": manifest})
|
|
81
|
+
be = await tool.build_backend()
|
|
82
|
+
fe = await tool.build_frontend()
|
|
83
|
+
assert "npm run build" in be
|
|
84
|
+
assert "npm run build" in fe
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.mark.asyncio
|
|
88
|
+
async def test_build_no_manifest_or_command():
|
|
89
|
+
tool = make_tool({})
|
|
90
|
+
be = await tool.build_backend()
|
|
91
|
+
fe = await tool.build_frontend()
|
|
92
|
+
assert "No backend_build_command or build_command" in be
|
|
93
|
+
assert "No frontend_build_command or build_command" in fe
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from kolega_code.config import AgentConfig, ModelConfig, ModelProvider, RateLimitConfig
|
|
7
|
+
from kolega_code.agent.tool_backend.create_file_tool import CreateFileTool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_connection_manager():
|
|
12
|
+
return AsyncMock()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def project_path(tmp_path):
|
|
17
|
+
return tmp_path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def agent_config():
|
|
22
|
+
return AgentConfig(
|
|
23
|
+
anthropic_api_key="test_key",
|
|
24
|
+
openai_api_key="test-key",
|
|
25
|
+
long_context_config=ModelConfig(
|
|
26
|
+
provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()
|
|
27
|
+
),
|
|
28
|
+
fast_config=ModelConfig(provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()),
|
|
29
|
+
thinking_config=ModelConfig(
|
|
30
|
+
provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig(), thinking_tokens=1024
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def mock_base_agent():
|
|
37
|
+
mock = Mock()
|
|
38
|
+
mock.agent_name = "test_agent"
|
|
39
|
+
return mock
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def create_file_tool(project_path, mock_connection_manager, agent_config, mock_base_agent):
|
|
44
|
+
return CreateFileTool(
|
|
45
|
+
project_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestCreateFileTool:
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_create_file_success(self, create_file_tool, project_path):
|
|
52
|
+
result = await create_file_tool.create_file("test.txt", "Hello World")
|
|
53
|
+
|
|
54
|
+
assert "File created successfully" in result
|
|
55
|
+
assert (project_path / "test.txt").exists()
|
|
56
|
+
assert (project_path / "test.txt").read_text() == "Hello World"
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_create_file_in_subdirectory(self, create_file_tool, project_path):
|
|
60
|
+
result = await create_file_tool.create_file("subdir/test.txt", "Hello World")
|
|
61
|
+
|
|
62
|
+
assert "File created successfully" in result
|
|
63
|
+
assert (project_path / "subdir" / "test.txt").exists()
|
|
64
|
+
assert (project_path / "subdir" / "test.txt").read_text() == "Hello World"
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_create_file_already_exists(self, create_file_tool, project_path):
|
|
68
|
+
# Create the file first
|
|
69
|
+
(project_path / "test.txt").write_text("Original content")
|
|
70
|
+
|
|
71
|
+
result = await create_file_tool.create_file("test.txt", "New content")
|
|
72
|
+
|
|
73
|
+
assert "File already exists" in result
|
|
74
|
+
assert (project_path / "test.txt").read_text() == "Original content"
|
|
75
|
+
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
async def test_create_file_parent_directory_does_not_exist(self, create_file_tool, project_path):
|
|
78
|
+
result = await create_file_tool.create_file("nonexistent/test.txt", "Hello World")
|
|
79
|
+
|
|
80
|
+
assert "File created successfully" in result
|
|
81
|
+
assert (project_path / "nonexistent" / "test.txt").exists()
|
|
82
|
+
assert (project_path / "nonexistent" / "test.txt").read_text() == "Hello World"
|
|
83
|
+
|
|
84
|
+
@pytest.mark.asyncio
|
|
85
|
+
async def test_create_file_permission_error(self, create_file_tool, project_path):
|
|
86
|
+
with patch("pathlib.Path.write_text", side_effect=PermissionError):
|
|
87
|
+
result = await create_file_tool.create_file("test.txt", "Hello World")
|
|
88
|
+
|
|
89
|
+
assert "Permission denied" in result
|
|
90
|
+
assert not (project_path / "test.txt").exists()
|
|
91
|
+
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_create_file_general_error(self, create_file_tool, project_path):
|
|
94
|
+
with patch("pathlib.Path.write_text", side_effect=Exception("General error")):
|
|
95
|
+
result = await create_file_tool.create_file("test.txt", "Hello World")
|
|
96
|
+
|
|
97
|
+
assert "Error creating file" in result
|
|
98
|
+
assert not (project_path / "test.txt").exists()
|
|
99
|
+
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
async def test_create_file_with_empty_content(self, create_file_tool, project_path):
|
|
102
|
+
result = await create_file_tool.create_file("test.txt", "")
|
|
103
|
+
|
|
104
|
+
assert "File created successfully" in result
|
|
105
|
+
assert (project_path / "test.txt").exists()
|
|
106
|
+
assert (project_path / "test.txt").read_text() == ""
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_create_file_with_multiline_content(self, create_file_tool, project_path):
|
|
110
|
+
content = "Line 1\nLine 2\nLine 3"
|
|
111
|
+
result = await create_file_tool.create_file("test.txt", content)
|
|
112
|
+
|
|
113
|
+
assert "File created successfully" in result
|
|
114
|
+
assert (project_path / "test.txt").exists()
|
|
115
|
+
assert (project_path / "test.txt").read_text() == content
|