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,549 @@
1
+ from pathlib import Path
2
+ from unittest.mock import AsyncMock, Mock, patch
3
+ import uuid
4
+
5
+ import pytest
6
+
7
+ from kolega_code.agent.baseagent import BaseAgent
8
+ from kolega_code.config import AgentConfig, ModelConfig, ModelProvider, RateLimitConfig
9
+ from kolega_code.events import AgentConnectionManager
10
+ from kolega_code.agent.tools import ToolCollection, ToolDefinition, ToolCollectionConfig
11
+
12
+
13
+ @pytest.fixture
14
+ def mock_connection_manager() -> AsyncMock:
15
+ return AsyncMock()
16
+
17
+
18
+ @pytest.fixture
19
+ def project_path(tmp_path: Path) -> Path:
20
+ return tmp_path
21
+
22
+
23
+ @pytest.fixture
24
+ def agent_config() -> AgentConfig:
25
+ return AgentConfig(
26
+ anthropic_api_key="test_key",
27
+ openai_api_key="test-key",
28
+ long_context_config=ModelConfig(
29
+ provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()
30
+ ),
31
+ fast_config=ModelConfig(provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()),
32
+ thinking_config=ModelConfig(
33
+ provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig(), thinking_tokens=1024
34
+ ),
35
+ )
36
+
37
+
38
+ @pytest.fixture
39
+ def mock_base_agent() -> Mock:
40
+ mock = Mock()
41
+ mock.agent_name = "test_agent"
42
+ return mock
43
+
44
+
45
+ @pytest.fixture
46
+ def tool_collection(
47
+ project_path: Path,
48
+ mock_connection_manager: AgentConnectionManager,
49
+ agent_config: AgentConfig,
50
+ mock_base_agent: BaseAgent,
51
+ ) -> ToolCollection:
52
+ # Create a ToolCollection with mocked tools
53
+ collection = ToolCollection(
54
+ project_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
55
+ )
56
+
57
+ # Mock all tool methods
58
+ collection.think_hard_tool.think_hard = AsyncMock()
59
+ collection.apply_edit_tool.edit_file = AsyncMock()
60
+ collection.search_and_replace_tool.search_and_replace = AsyncMock()
61
+ collection.list_directory_tool.list_directory = AsyncMock()
62
+ collection.terminal_tool.execute_terminal_command = AsyncMock()
63
+ collection.read_file_tool.read_entire_file = AsyncMock()
64
+ collection.read_file_tool.read_file_section = AsyncMock()
65
+ collection.create_file_tool.create_file = AsyncMock()
66
+ collection.replace_entire_file_tool.replace_entire_file = AsyncMock()
67
+ collection.replace_lines_tool.replace_lines = AsyncMock()
68
+ collection.apply_patch_tool.apply_patch = AsyncMock()
69
+ collection.memory_tool.read_memory = AsyncMock()
70
+ collection.memory_tool.write_memory = AsyncMock()
71
+ collection.search_codebase_tool.search_codebase = AsyncMock()
72
+ collection.glob_tool.find_files_by_pattern = AsyncMock()
73
+ collection.web_fetch_tool.web_fetch = AsyncMock()
74
+ collection.terminal_tool.send_terminal_input = AsyncMock()
75
+
76
+ return collection
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ class TestToolCollection:
81
+ """Test cases for the ToolCollection class"""
82
+
83
+ async def test_initialization_with_valid_path(
84
+ self,
85
+ project_path: Path,
86
+ mock_connection_manager: AgentConnectionManager,
87
+ agent_config: AgentConfig,
88
+ mock_base_agent: BaseAgent,
89
+ ) -> None:
90
+ tool_collection = ToolCollection(
91
+ project_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
92
+ )
93
+ assert tool_collection.project_path == project_path
94
+ assert tool_collection.workspace_id == "test_workspace"
95
+ assert tool_collection.connection_manager == mock_connection_manager
96
+
97
+ async def test_initialization_with_string_path(
98
+ self,
99
+ tmp_path: Path,
100
+ mock_connection_manager: AgentConnectionManager,
101
+ agent_config: AgentConfig,
102
+ mock_base_agent: BaseAgent,
103
+ ) -> None:
104
+ tool_collection = ToolCollection(
105
+ str(tmp_path), "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
106
+ )
107
+ assert tool_collection.project_path == tmp_path
108
+
109
+ async def test_initialization_with_nonexistent_path(
110
+ self, mock_connection_manager: AgentConnectionManager, agent_config: AgentConfig, mock_base_agent: BaseAgent
111
+ ) -> None:
112
+ # Local filesystems validate their root eagerly
113
+ with pytest.raises(ValueError) as exc_info:
114
+ ToolCollection(
115
+ "/nonexistent/path",
116
+ "test_workspace",
117
+ str(uuid.uuid4()),
118
+ mock_connection_manager,
119
+ agent_config,
120
+ mock_base_agent,
121
+ )
122
+ assert "Project path does not exist" in str(exc_info.value)
123
+
124
+ async def test_initialization_with_file_path(
125
+ self,
126
+ tmp_path: Path,
127
+ mock_connection_manager: AgentConnectionManager,
128
+ agent_config: AgentConfig,
129
+ mock_base_agent: BaseAgent,
130
+ ) -> None:
131
+ file_path = tmp_path / "test.txt"
132
+ file_path.touch()
133
+ # Local filesystems validate their root eagerly
134
+ with pytest.raises(ValueError) as exc_info:
135
+ ToolCollection(
136
+ file_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
137
+ )
138
+ assert "Project path is not a directory" in str(exc_info.value)
139
+
140
+ async def test_initialization_with_nonexistent_path_sandbox_filesystem(
141
+ self, mock_connection_manager: AgentConnectionManager, agent_config: AgentConfig, mock_base_agent: BaseAgent
142
+ ) -> None:
143
+ """Sandbox filesystems don't validate their root locally (validate_root is a no-op)."""
144
+ sandbox_fs = Mock()
145
+ sandbox_fs.validate_root = Mock(return_value=None)
146
+ tool_collection = ToolCollection(
147
+ "/nonexistent/path",
148
+ "test_workspace",
149
+ str(uuid.uuid4()),
150
+ mock_connection_manager,
151
+ agent_config,
152
+ mock_base_agent,
153
+ filesystem=sandbox_fs,
154
+ )
155
+ sandbox_fs.validate_root.assert_called_once()
156
+ assert tool_collection.workspace_id == "test_workspace"
157
+ assert str(tool_collection.project_path) == "/nonexistent/path"
158
+
159
+ async def test_initialization_with_file_path_sandbox_filesystem(
160
+ self,
161
+ tmp_path: Path,
162
+ mock_connection_manager: AgentConnectionManager,
163
+ agent_config: AgentConfig,
164
+ mock_base_agent: BaseAgent,
165
+ ) -> None:
166
+ """Sandbox filesystems accept any project path; the sandbox provisions the root."""
167
+ file_path = tmp_path / "test.txt"
168
+ file_path.touch()
169
+
170
+ sandbox_fs = Mock()
171
+ sandbox_fs.validate_root = Mock(return_value=None)
172
+ tool_collection = ToolCollection(
173
+ file_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent,
174
+ filesystem=sandbox_fs,
175
+ )
176
+ sandbox_fs.validate_root.assert_called_once()
177
+ assert tool_collection.workspace_id == "test_workspace"
178
+ assert tool_collection.project_path == file_path
179
+
180
+ async def test_think_hard(self, tool_collection: AsyncMock) -> None:
181
+ problem = "Test problem"
182
+ expected_response = "Test response"
183
+ tool_collection.think_hard_tool.think_hard.return_value = expected_response
184
+
185
+ result = await tool_collection.think_hard(problem)
186
+ assert result == expected_response
187
+ tool_collection.think_hard_tool.think_hard.assert_called_once_with(problem)
188
+
189
+ async def test_edit_file(self, tool_collection: AsyncMock) -> None:
190
+ relative_path = "test.txt"
191
+ instructions = "instructions"
192
+ code_edit = "test content"
193
+ expected_response = "Updated content"
194
+ tool_collection.apply_edit_tool.edit_file.return_value = expected_response
195
+
196
+ result = await tool_collection.edit_file(relative_path, instructions, code_edit)
197
+ assert result == expected_response
198
+ tool_collection.apply_edit_tool.edit_file.assert_called_once_with(relative_path, instructions, code_edit)
199
+
200
+ async def test_search_and_replace(self, tool_collection: AsyncMock) -> None:
201
+ relative_path = "test.txt"
202
+ block = "<<<<<<< SEARCH\nold\n======\nnew\n>>>>>>> REPLACE"
203
+ expected_response = "Updated content"
204
+ tool_collection.search_and_replace_tool.search_and_replace.return_value = expected_response
205
+
206
+ result = await tool_collection.search_and_replace(relative_path, block)
207
+ assert result == expected_response
208
+ tool_collection.search_and_replace_tool.search_and_replace.assert_called_once_with(relative_path, block)
209
+
210
+ async def test_list_directory(self, tool_collection: AsyncMock) -> None:
211
+ relative_path = "test_dir"
212
+ expected_response = "Directory listing"
213
+ tool_collection.list_directory_tool.list_directory.return_value = expected_response
214
+
215
+ result = await tool_collection.list_directory(relative_path)
216
+ assert result == expected_response
217
+ tool_collection.list_directory_tool.list_directory.assert_called_once_with(relative_path)
218
+
219
+ async def test_execute_terminal_command(self, tool_collection: AsyncMock) -> None:
220
+ command = "ls -la"
221
+ expected_response = "Command output"
222
+ tool_collection.terminal_tool.execute_terminal_command.return_value = expected_response
223
+
224
+ result = await tool_collection.execute_terminal_command(command)
225
+ assert result == expected_response
226
+ tool_collection.terminal_tool.execute_terminal_command.assert_called_once_with(command)
227
+
228
+ async def test_send_terminal_input(self, tool_collection: AsyncMock) -> None:
229
+ expected_response = "Sent input"
230
+ tool_collection.terminal_tool.send_terminal_input.return_value = expected_response
231
+
232
+ result = await tool_collection.send_terminal_input("terminal_1", "Ada", submit=True, command_id="cmd_1")
233
+
234
+ assert result == expected_response
235
+ tool_collection.terminal_tool.send_terminal_input.assert_called_once_with(
236
+ "terminal_1", "Ada", submit=True, command_id="cmd_1"
237
+ )
238
+
239
+ async def test_read_entire_file(self, tool_collection: AsyncMock) -> None:
240
+ relative_path = "test.txt"
241
+ expected_response = "File content"
242
+ tool_collection.read_file_tool.read_entire_file.return_value = expected_response
243
+
244
+ result = await tool_collection.read_entire_file(relative_path)
245
+ assert result == expected_response
246
+ tool_collection.read_file_tool.read_entire_file.assert_called_once_with(relative_path)
247
+
248
+ async def test_read_file_section(self, tool_collection: AsyncMock) -> None:
249
+ relative_path = "test.txt"
250
+ start_line = 1
251
+ end_line = 10
252
+ expected_response = "File section"
253
+ tool_collection.read_file_tool.read_file_section.return_value = expected_response
254
+
255
+ result = await tool_collection.read_file_section(relative_path, start_line, end_line)
256
+ assert result == expected_response
257
+ tool_collection.read_file_tool.read_file_section.assert_called_once_with(relative_path, start_line, end_line)
258
+
259
+ async def test_create_file(self, tool_collection: AsyncMock) -> None:
260
+ relative_path = "test.txt"
261
+ content = "New file content"
262
+ expected_response = "Created file content"
263
+ tool_collection.create_file_tool.create_file.return_value = expected_response
264
+
265
+ result = await tool_collection.create_file(relative_path, content)
266
+ assert result == expected_response
267
+ tool_collection.create_file_tool.create_file.assert_called_once_with(relative_path, content)
268
+
269
+ async def test_replace_entire_file(self, tool_collection: AsyncMock) -> None:
270
+ relative_path = "test.txt"
271
+ content = "New content"
272
+ expected_response = "Updated content"
273
+ tool_collection.replace_entire_file_tool.replace_entire_file.return_value = expected_response
274
+
275
+ result = await tool_collection.replace_entire_file(relative_path, content)
276
+ assert result == expected_response
277
+ tool_collection.replace_entire_file_tool.replace_entire_file.assert_called_once_with(relative_path, content)
278
+
279
+ async def test_replace_lines(self, tool_collection: AsyncMock) -> None:
280
+ relative_path = "test.txt"
281
+ start_line = 1
282
+ end_line = 5
283
+ new_content = "New lines"
284
+ expected_response = "Updated content"
285
+ tool_collection.replace_lines_tool.replace_lines.return_value = expected_response
286
+
287
+ result = await tool_collection.replace_lines(relative_path, start_line, end_line, new_content)
288
+ assert result == expected_response
289
+ tool_collection.replace_lines_tool.replace_lines.assert_called_once_with(
290
+ relative_path, start_line, end_line, new_content
291
+ )
292
+
293
+ async def test_apply_patch(self, tool_collection: AsyncMock) -> None:
294
+ patch_content = "diff content"
295
+ expected_response = "Patched content"
296
+ tool_collection.apply_patch_tool.apply_patch.return_value = expected_response
297
+
298
+ result = await tool_collection.apply_patch(patch_content)
299
+ assert result == expected_response
300
+ tool_collection.apply_patch_tool.apply_patch.assert_called_once_with(patch_content)
301
+
302
+ async def test_read_memory(self, tool_collection: AsyncMock) -> None:
303
+ expected_response = "Memory content"
304
+ tool_collection.memory_tool.read_memory.return_value = expected_response
305
+
306
+ result = await tool_collection.read_memory()
307
+ assert result == expected_response
308
+ tool_collection.memory_tool.read_memory.assert_called_once()
309
+
310
+ async def test_write_memory(self, tool_collection: AsyncMock) -> None:
311
+ memory_content = "New memory"
312
+ expected_response = "Success"
313
+ tool_collection.memory_tool.write_memory.return_value = expected_response
314
+
315
+ result = await tool_collection.write_memory(memory_content)
316
+ assert result == expected_response
317
+ tool_collection.memory_tool.write_memory.assert_called_once_with(memory_content)
318
+
319
+ async def test_search_codebase(self, tool_collection: AsyncMock) -> None:
320
+ pattern = "test"
321
+ file_pattern = "*.py"
322
+ case_sensitive = True
323
+ expected_response = "Search results"
324
+ tool_collection.search_codebase_tool.search_codebase.return_value = expected_response
325
+
326
+ result = await tool_collection.search_codebase(pattern, file_pattern, case_sensitive)
327
+ assert result == expected_response
328
+ tool_collection.search_codebase_tool.search_codebase.assert_called_once_with(
329
+ pattern, file_pattern=file_pattern, case_sensitive=case_sensitive, literal=True
330
+ )
331
+
332
+ async def test_web_fetch(self, tool_collection: AsyncMock) -> None:
333
+ url = "https://example.com"
334
+ instruction = "Summarize this page"
335
+ expected_response = "Summary"
336
+ tool_collection.web_fetch_tool.web_fetch.return_value = expected_response
337
+
338
+ result = await tool_collection.web_fetch(url, instruction)
339
+
340
+ assert result == expected_response
341
+ tool_collection.web_fetch_tool.web_fetch.assert_called_once_with(url, instruction)
342
+
343
+ @pytest.mark.asyncio
344
+ async def test_find_files_by_pattern(self, tool_collection: AsyncMock) -> None:
345
+ pattern = "*.py"
346
+ include_directories = True
347
+ show_details = False
348
+ expected_response = "File list"
349
+ tool_collection.glob_tool.find_files_by_pattern.return_value = expected_response
350
+
351
+ result = await tool_collection.find_files_by_pattern(pattern, include_directories, show_details)
352
+ assert result == expected_response
353
+ tool_collection.glob_tool.find_files_by_pattern.assert_called_once_with(
354
+ pattern, include_directories=include_directories, show_details=show_details
355
+ )
356
+
357
+ async def test_get_tool_list(self, tool_collection: AsyncMock) -> None:
358
+ tool_list = tool_collection.get_tool_list()
359
+ assert isinstance(tool_list, list)
360
+ assert len(tool_list) > 0
361
+
362
+ # Check that each tool has required fields
363
+ for tool in tool_list:
364
+ assert isinstance(tool, ToolDefinition)
365
+ assert hasattr(tool, "name")
366
+ assert hasattr(tool, "description")
367
+ assert hasattr(tool, "parameters")
368
+ assert isinstance(tool.parameters, list)
369
+ for param in tool.parameters:
370
+ assert hasattr(param, "name")
371
+ assert hasattr(param, "type")
372
+ assert hasattr(param, "description")
373
+ assert hasattr(param, "required")
374
+
375
+ # Check that excluded tools are not in the list
376
+ excluded_tools = tool_collection.tool_exclusions
377
+ tool_names = [tool.name for tool in tool_list]
378
+ assert "send_terminal_input" in tool_names
379
+ for excluded_tool in excluded_tools:
380
+ assert excluded_tool not in tool_names
381
+
382
+ async def test_tool_collection_config_read_only(
383
+ self,
384
+ project_path: Path,
385
+ mock_connection_manager: AgentConnectionManager,
386
+ agent_config: AgentConfig,
387
+ mock_base_agent: BaseAgent,
388
+ ) -> None:
389
+ """Test that read_only configuration properly filters tools."""
390
+ config = ToolCollectionConfig(read_only=True)
391
+ tool_collection = ToolCollection(
392
+ project_path,
393
+ "test_workspace",
394
+ str(uuid.uuid4()),
395
+ mock_connection_manager,
396
+ agent_config,
397
+ mock_base_agent,
398
+ tool_config=config,
399
+ )
400
+
401
+ tool_list = tool_collection.get_tool_list()
402
+ tool_names = [tool.name for tool in tool_list]
403
+
404
+ # Should only include read-only tools
405
+ for tool_name in tool_names:
406
+ assert tool_name in ToolCollection.read_only_tools
407
+
408
+ # Should not include write tools
409
+ write_tools = ["create_file", "replace_entire_file", "edit_file"]
410
+ for write_tool in write_tools:
411
+ assert write_tool not in tool_names
412
+
413
+ async def test_tool_collection_config_browser_only(
414
+ self,
415
+ project_path: Path,
416
+ mock_connection_manager: AgentConnectionManager,
417
+ agent_config: AgentConfig,
418
+ mock_base_agent: BaseAgent,
419
+ ) -> None:
420
+ """Test that browser_only configuration properly filters tools."""
421
+ config = ToolCollectionConfig(browser_only=True)
422
+ tool_collection = ToolCollection(
423
+ project_path,
424
+ "test_workspace",
425
+ str(uuid.uuid4()),
426
+ mock_connection_manager,
427
+ agent_config,
428
+ mock_base_agent,
429
+ tool_config=config,
430
+ )
431
+
432
+ tool_list = tool_collection.get_tool_list()
433
+ tool_names = [tool.name for tool in tool_list]
434
+
435
+ # Should only include browser tools
436
+ for tool_name in tool_names:
437
+ assert tool_name in ToolCollection.browser_tools
438
+
439
+ # Should not include file tools
440
+ file_tools = ["read_entire_file", "create_file", "list_directory"]
441
+ for file_tool in file_tools:
442
+ assert file_tool not in tool_names
443
+
444
+ async def test_tool_collection_extension_tools(
445
+ self,
446
+ project_path: Path,
447
+ mock_connection_manager: AgentConnectionManager,
448
+ agent_config: AgentConfig,
449
+ mock_base_agent: BaseAgent,
450
+ ) -> None:
451
+ """Test that host-provided extension tools can be included by group."""
452
+ from kolega_code.agent.tools import ToolExtension
453
+
454
+ async def custom_status() -> str:
455
+ """Return custom host status."""
456
+ return "ok"
457
+
458
+ extension = ToolExtension(
459
+ name="test-extension",
460
+ tools={"custom_status": custom_status},
461
+ tool_groups={"host_tools": ["custom_status"]},
462
+ )
463
+ config = ToolCollectionConfig(custom_tool_groups=["host_tools"], restrict_to_tool_groups=True)
464
+ tool_collection = ToolCollection(
465
+ project_path,
466
+ "test_workspace",
467
+ str(uuid.uuid4()),
468
+ mock_connection_manager,
469
+ agent_config,
470
+ mock_base_agent,
471
+ tool_config=config,
472
+ tool_extensions=[extension],
473
+ )
474
+
475
+ tool_list = tool_collection.get_tool_list()
476
+ tool_names = [tool.name for tool in tool_list]
477
+
478
+ assert tool_names == ["custom_status"]
479
+
480
+ async def test_tool_collection_config_mixed_options(
481
+ self,
482
+ project_path: Path,
483
+ mock_connection_manager: AgentConnectionManager,
484
+ agent_config: AgentConfig,
485
+ mock_base_agent: BaseAgent,
486
+ ) -> None:
487
+ """Test combinations of configuration options work correctly."""
488
+ config = ToolCollectionConfig(include_agent_dispatch_tools=True, tool_exclusions=["think_hard"])
489
+ tool_collection = ToolCollection(
490
+ project_path,
491
+ "test_workspace",
492
+ str(uuid.uuid4()),
493
+ mock_connection_manager,
494
+ agent_config,
495
+ mock_base_agent,
496
+ tool_config=config,
497
+ )
498
+
499
+ tool_list = tool_collection.get_tool_list()
500
+ tool_names = [tool.name for tool in tool_list]
501
+
502
+ # Should exclude explicitly excluded tools
503
+ assert "think_hard" not in tool_names
504
+
505
+ # Should include investigation tools
506
+ assert "dispatch_investigation_agent" in tool_names
507
+ assert "dispatch_browser_agent" in tool_names
508
+
509
+ async def test_backward_compatibility_legacy_parameters(
510
+ self,
511
+ project_path: Path,
512
+ mock_connection_manager: AgentConnectionManager,
513
+ agent_config: AgentConfig,
514
+ mock_base_agent: BaseAgent,
515
+ ) -> None:
516
+ """Test that legacy read_only and browser_only parameters still work."""
517
+ # Test legacy read_only parameter
518
+ tool_collection_ro = ToolCollection(
519
+ project_path,
520
+ "test_workspace",
521
+ str(uuid.uuid4()),
522
+ mock_connection_manager,
523
+ agent_config,
524
+ mock_base_agent,
525
+ read_only=True,
526
+ )
527
+
528
+ tool_list_ro = tool_collection_ro.get_tool_list()
529
+ tool_names_ro = [tool.name for tool in tool_list_ro]
530
+
531
+ for tool_name in tool_names_ro:
532
+ assert tool_name in ToolCollection.read_only_tools
533
+
534
+ # Test legacy browser_only parameter
535
+ tool_collection_browser = ToolCollection(
536
+ project_path,
537
+ "test_workspace",
538
+ str(uuid.uuid4()),
539
+ mock_connection_manager,
540
+ agent_config,
541
+ mock_base_agent,
542
+ browser_only=True,
543
+ )
544
+
545
+ tool_list_browser = tool_collection_browser.get_tool_list()
546
+ tool_names_browser = [tool.name for tool in tool_list_browser]
547
+
548
+ for tool_name in tool_names_browser:
549
+ assert tool_name in ToolCollection.browser_tools
File without changes