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,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