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