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,292 @@
|
|
|
1
|
+
"""Unit tests for the ListDirectoryTool with local filesystem."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import pytest_asyncio
|
|
5
|
+
import tempfile
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import Mock, AsyncMock
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from kolega_code.services.file_system import LocalFileSystem
|
|
12
|
+
from kolega_code.agent.tool_backend.list_directory_tool import ListDirectoryTool
|
|
13
|
+
from kolega_code.agent.baseagent import BaseAgent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestListDirectoryTool:
|
|
17
|
+
"""Unit tests for list directory tool with local filesystem."""
|
|
18
|
+
|
|
19
|
+
@pytest_asyncio.fixture
|
|
20
|
+
async def test_directory(self):
|
|
21
|
+
"""Create a temporary directory with test files."""
|
|
22
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
23
|
+
# Create test structure
|
|
24
|
+
os.makedirs(os.path.join(tmpdir, ".git", "objects"))
|
|
25
|
+
os.makedirs(os.path.join(tmpdir, "src", "components"))
|
|
26
|
+
os.makedirs(os.path.join(tmpdir, "docs"))
|
|
27
|
+
os.makedirs(os.path.join(tmpdir, "tests"))
|
|
28
|
+
|
|
29
|
+
# Create files
|
|
30
|
+
Path(os.path.join(tmpdir, "main.py")).write_text('print("Hello")')
|
|
31
|
+
Path(os.path.join(tmpdir, "README.md")).write_text("# Project")
|
|
32
|
+
Path(os.path.join(tmpdir, "package.json")).write_text('{"name": "test"}')
|
|
33
|
+
Path(os.path.join(tmpdir, "Dockerfile")).write_text("FROM python:3.9")
|
|
34
|
+
Path(os.path.join(tmpdir, ".gitignore")).write_text("*.pyc\n__pycache__/")
|
|
35
|
+
Path(os.path.join(tmpdir, "requirements.txt")).write_text("pytest==7.0.0")
|
|
36
|
+
|
|
37
|
+
# Create files in subdirectories
|
|
38
|
+
Path(os.path.join(tmpdir, ".git", "config")).write_text("[core]\n")
|
|
39
|
+
Path(os.path.join(tmpdir, "src", "app.py")).write_text("def main(): pass")
|
|
40
|
+
Path(os.path.join(tmpdir, "src", "components", "button.py")).write_text("class Button: pass")
|
|
41
|
+
Path(os.path.join(tmpdir, "docs", "api.md")).write_text("# API")
|
|
42
|
+
Path(os.path.join(tmpdir, "tests", "test_main.py")).write_text("def test_main(): pass")
|
|
43
|
+
|
|
44
|
+
# Create a larger file
|
|
45
|
+
with open(os.path.join(tmpdir, "large.dat"), "wb") as f:
|
|
46
|
+
f.write(b"0" * (1024 * 1024)) # 1MB file
|
|
47
|
+
|
|
48
|
+
yield tmpdir
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def mock_agent(self):
|
|
52
|
+
"""Create a mock agent for the tool."""
|
|
53
|
+
agent = Mock(spec=BaseAgent)
|
|
54
|
+
agent.agent_name = "test-agent"
|
|
55
|
+
agent.log_info = AsyncMock()
|
|
56
|
+
agent.log_error = AsyncMock()
|
|
57
|
+
return agent
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_list_root_excludes_git(self, test_directory, mock_agent):
|
|
61
|
+
"""Test that .git directory is excluded from root listing."""
|
|
62
|
+
# Create filesystem and tool
|
|
63
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
64
|
+
tool = ListDirectoryTool(
|
|
65
|
+
project_path=test_directory,
|
|
66
|
+
workspace_id="test",
|
|
67
|
+
thread_id="test",
|
|
68
|
+
connection_manager=Mock(),
|
|
69
|
+
config=Mock(),
|
|
70
|
+
caller=mock_agent,
|
|
71
|
+
filesystem=filesystem,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# List root directory
|
|
75
|
+
result = await tool.list_directory("")
|
|
76
|
+
|
|
77
|
+
# Verify .git is not in the output
|
|
78
|
+
assert "| 📁 | .git" not in result
|
|
79
|
+
|
|
80
|
+
# Verify other directories are present
|
|
81
|
+
assert "| 📁 | src" in result
|
|
82
|
+
assert "| 📁 | docs" in result
|
|
83
|
+
assert "| 📁 | tests" in result
|
|
84
|
+
|
|
85
|
+
# Verify files are present
|
|
86
|
+
assert "| 📄 | main.py" in result
|
|
87
|
+
assert "| 📄 | README.md" in result
|
|
88
|
+
assert "| 📄 | .gitignore" in result
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_file_descriptions(self, test_directory, mock_agent):
|
|
92
|
+
"""Test that file descriptions are correct."""
|
|
93
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
94
|
+
tool = ListDirectoryTool(
|
|
95
|
+
project_path=test_directory,
|
|
96
|
+
workspace_id="test",
|
|
97
|
+
thread_id="test",
|
|
98
|
+
connection_manager=Mock(),
|
|
99
|
+
config=Mock(),
|
|
100
|
+
caller=mock_agent,
|
|
101
|
+
filesystem=filesystem,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
result = await tool.list_directory("")
|
|
105
|
+
|
|
106
|
+
# Check file descriptions
|
|
107
|
+
assert "Python Source |" in result # main.py
|
|
108
|
+
assert "Project Documentation |" in result # README.md
|
|
109
|
+
assert "Node.js Package |" in result # package.json
|
|
110
|
+
assert "Docker Definition |" in result # Dockerfile
|
|
111
|
+
assert "Git Ignore Rules |" in result # .gitignore
|
|
112
|
+
assert "Python Dependencies |" in result # requirements.txt
|
|
113
|
+
assert "DAT File |" in result # large.dat
|
|
114
|
+
|
|
115
|
+
@pytest.mark.asyncio
|
|
116
|
+
async def test_file_sizes(self, test_directory, mock_agent):
|
|
117
|
+
"""Test that file sizes are displayed correctly."""
|
|
118
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
119
|
+
tool = ListDirectoryTool(
|
|
120
|
+
project_path=test_directory,
|
|
121
|
+
workspace_id="test",
|
|
122
|
+
thread_id="test",
|
|
123
|
+
connection_manager=Mock(),
|
|
124
|
+
config=Mock(),
|
|
125
|
+
caller=mock_agent,
|
|
126
|
+
filesystem=filesystem,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
result = await tool.list_directory("")
|
|
130
|
+
|
|
131
|
+
# The large.dat file should show as 1.0 MB
|
|
132
|
+
lines = result.split("\n")
|
|
133
|
+
large_file_line = [line for line in lines if "large.dat" in line][0]
|
|
134
|
+
assert "1.0 MB" in large_file_line
|
|
135
|
+
|
|
136
|
+
# Small files should show in bytes
|
|
137
|
+
main_py_line = [line for line in lines if "main.py" in line][0]
|
|
138
|
+
assert " B |" in main_py_line
|
|
139
|
+
|
|
140
|
+
@pytest.mark.asyncio
|
|
141
|
+
async def test_directory_item_counts(self, test_directory, mock_agent):
|
|
142
|
+
"""Test that directory item counts are correct."""
|
|
143
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
144
|
+
tool = ListDirectoryTool(
|
|
145
|
+
project_path=test_directory,
|
|
146
|
+
workspace_id="test",
|
|
147
|
+
thread_id="test",
|
|
148
|
+
connection_manager=Mock(),
|
|
149
|
+
config=Mock(),
|
|
150
|
+
caller=mock_agent,
|
|
151
|
+
filesystem=filesystem,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
result = await tool.list_directory("")
|
|
155
|
+
|
|
156
|
+
# Check directory item counts
|
|
157
|
+
lines = result.split("\n")
|
|
158
|
+
src_line = [line for line in lines if "| 📁 | src" in line][0]
|
|
159
|
+
assert "2 items" in src_line # app.py and components/
|
|
160
|
+
|
|
161
|
+
docs_line = [line for line in lines if "| 📁 | docs" in line][0]
|
|
162
|
+
assert "1 items" in docs_line # api.md
|
|
163
|
+
|
|
164
|
+
tests_line = [line for line in lines if "| 📁 | tests" in line][0]
|
|
165
|
+
assert "1 items" in tests_line # test_main.py
|
|
166
|
+
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
async def test_subdirectory_listing(self, test_directory, mock_agent):
|
|
169
|
+
"""Test listing a subdirectory."""
|
|
170
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
171
|
+
tool = ListDirectoryTool(
|
|
172
|
+
project_path=test_directory,
|
|
173
|
+
workspace_id="test",
|
|
174
|
+
thread_id="test",
|
|
175
|
+
connection_manager=Mock(),
|
|
176
|
+
config=Mock(),
|
|
177
|
+
caller=mock_agent,
|
|
178
|
+
filesystem=filesystem,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
result = await tool.list_directory("src")
|
|
182
|
+
|
|
183
|
+
# Verify header and navigation
|
|
184
|
+
assert "# Directory: src" in result
|
|
185
|
+
assert "📁 Root Directory" in result
|
|
186
|
+
|
|
187
|
+
# Verify contents
|
|
188
|
+
assert "| 📄 | app.py" in result
|
|
189
|
+
assert "| 📁 | components" in result
|
|
190
|
+
assert "Python Source |" in result
|
|
191
|
+
|
|
192
|
+
@pytest.mark.asyncio
|
|
193
|
+
async def test_nested_directory(self, test_directory, mock_agent):
|
|
194
|
+
"""Test listing a nested directory."""
|
|
195
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
196
|
+
tool = ListDirectoryTool(
|
|
197
|
+
project_path=test_directory,
|
|
198
|
+
workspace_id="test",
|
|
199
|
+
thread_id="test",
|
|
200
|
+
connection_manager=Mock(),
|
|
201
|
+
config=Mock(),
|
|
202
|
+
caller=mock_agent,
|
|
203
|
+
filesystem=filesystem,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
result = await tool.list_directory("src/components")
|
|
207
|
+
|
|
208
|
+
# Verify header and navigation
|
|
209
|
+
assert "# Directory: src/components" in result
|
|
210
|
+
assert "📁 Parent Directory: src" in result
|
|
211
|
+
|
|
212
|
+
# Verify contents
|
|
213
|
+
assert "| 📄 | button.py" in result
|
|
214
|
+
assert "Python Source |" in result
|
|
215
|
+
|
|
216
|
+
@pytest.mark.asyncio
|
|
217
|
+
async def test_nonexistent_directory(self, test_directory, mock_agent):
|
|
218
|
+
"""Test error handling for non-existent directory."""
|
|
219
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
220
|
+
tool = ListDirectoryTool(
|
|
221
|
+
project_path=test_directory,
|
|
222
|
+
workspace_id="test",
|
|
223
|
+
thread_id="test",
|
|
224
|
+
connection_manager=Mock(),
|
|
225
|
+
config=Mock(),
|
|
226
|
+
caller=mock_agent,
|
|
227
|
+
filesystem=filesystem,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
with pytest.raises(FileNotFoundError) as exc_info:
|
|
231
|
+
await tool.list_directory("nonexistent")
|
|
232
|
+
|
|
233
|
+
assert "Directory not found: nonexistent" in str(exc_info.value)
|
|
234
|
+
|
|
235
|
+
@pytest.mark.asyncio
|
|
236
|
+
async def test_list_file_instead_of_directory(self, test_directory, mock_agent):
|
|
237
|
+
"""Test error when trying to list a file."""
|
|
238
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
239
|
+
tool = ListDirectoryTool(
|
|
240
|
+
project_path=test_directory,
|
|
241
|
+
workspace_id="test",
|
|
242
|
+
thread_id="test",
|
|
243
|
+
connection_manager=Mock(),
|
|
244
|
+
config=Mock(),
|
|
245
|
+
caller=mock_agent,
|
|
246
|
+
filesystem=filesystem,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
with pytest.raises(NotADirectoryError) as exc_info:
|
|
250
|
+
await tool.list_directory("main.py")
|
|
251
|
+
|
|
252
|
+
assert "Not a directory: main.py" in str(exc_info.value)
|
|
253
|
+
|
|
254
|
+
@pytest.mark.asyncio
|
|
255
|
+
async def test_summary_counts(self, test_directory, mock_agent):
|
|
256
|
+
"""Test that summary counts are correct."""
|
|
257
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
258
|
+
tool = ListDirectoryTool(
|
|
259
|
+
project_path=test_directory,
|
|
260
|
+
workspace_id="test",
|
|
261
|
+
thread_id="test",
|
|
262
|
+
connection_manager=Mock(),
|
|
263
|
+
config=Mock(),
|
|
264
|
+
caller=mock_agent,
|
|
265
|
+
filesystem=filesystem,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
result = await tool.list_directory("")
|
|
269
|
+
|
|
270
|
+
# Verify summary - should be 3 directories (not counting .git) and 7 files
|
|
271
|
+
assert "**Summary:** 3 directories, 7 files" in result
|
|
272
|
+
|
|
273
|
+
@pytest.mark.asyncio
|
|
274
|
+
async def test_dates_shown(self, test_directory, mock_agent):
|
|
275
|
+
"""Test that modification dates are shown."""
|
|
276
|
+
filesystem = LocalFileSystem(root_path=test_directory)
|
|
277
|
+
tool = ListDirectoryTool(
|
|
278
|
+
project_path=test_directory,
|
|
279
|
+
workspace_id="test",
|
|
280
|
+
thread_id="test",
|
|
281
|
+
connection_manager=Mock(),
|
|
282
|
+
config=Mock(),
|
|
283
|
+
caller=mock_agent,
|
|
284
|
+
filesystem=filesystem,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
result = await tool.list_directory("")
|
|
288
|
+
|
|
289
|
+
# Dates should be in YYYY-MM-DD HH:MM format
|
|
290
|
+
current_year = datetime.now().year
|
|
291
|
+
assert f"{current_year}-" in result
|
|
292
|
+
assert "Unknown" not in result # No unknown dates
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, Mock
|
|
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.read_file_tool import ReadFileTool
|
|
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
|
+
return Mock()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def read_file_tool(project_path, mock_connection_manager, agent_config, mock_base_agent):
|
|
42
|
+
return ReadFileTool(
|
|
43
|
+
project_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def sample_file(project_path):
|
|
49
|
+
file_path = project_path / "test.txt"
|
|
50
|
+
file_path.write_text("Line 1\nLine 2\nLine 3\nLine 4\nLine 5")
|
|
51
|
+
return file_path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
class TestReadFileTool:
|
|
56
|
+
async def test_read_entire_file(self, read_file_tool, sample_file):
|
|
57
|
+
content = await read_file_tool.read_entire_file("test.txt")
|
|
58
|
+
expected = "# test.txt\n\n```\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\n```"
|
|
59
|
+
assert content == expected
|
|
60
|
+
|
|
61
|
+
async def test_read_entire_file_not_found(self, read_file_tool):
|
|
62
|
+
with pytest.raises(FileNotFoundError) as exc_info:
|
|
63
|
+
await read_file_tool.read_entire_file("nonexistent.txt")
|
|
64
|
+
assert str(exc_info.value) == "File not found: nonexistent.txt"
|
|
65
|
+
|
|
66
|
+
async def test_read_file_section(self, read_file_tool, sample_file):
|
|
67
|
+
content = await read_file_tool.read_file_section("test.txt", 2, 4)
|
|
68
|
+
expected = "# test.txt (lines 2-4)\n\n```\nLine 2\nLine 3\nLine 4\n\n```"
|
|
69
|
+
assert content == expected
|
|
70
|
+
|
|
71
|
+
async def test_read_file_section_single_line(self, read_file_tool, sample_file):
|
|
72
|
+
content = await read_file_tool.read_file_section("test.txt", 1, 1)
|
|
73
|
+
expected = "# test.txt (lines 1-1)\n\n```\nLine 1\n\n```"
|
|
74
|
+
assert content == expected
|
|
75
|
+
|
|
76
|
+
async def test_read_file_section_not_found(self, read_file_tool):
|
|
77
|
+
with pytest.raises(FileNotFoundError) as exc_info:
|
|
78
|
+
await read_file_tool.read_file_section("nonexistent.txt", 1, 1)
|
|
79
|
+
assert str(exc_info.value) == "File not found: nonexistent.txt"
|
|
80
|
+
|
|
81
|
+
async def test_read_file_section_invalid_start_line(self, read_file_tool, sample_file):
|
|
82
|
+
with pytest.raises(ValueError) as exc_info:
|
|
83
|
+
await read_file_tool.read_file_section("test.txt", 0, 1)
|
|
84
|
+
assert str(exc_info.value) == "Start line must be at least 1, got 0"
|
|
85
|
+
|
|
86
|
+
async def test_read_file_section_invalid_end_line(self, read_file_tool, sample_file):
|
|
87
|
+
with pytest.raises(ValueError) as exc_info:
|
|
88
|
+
await read_file_tool.read_file_section("test.txt", 3, 2)
|
|
89
|
+
assert str(exc_info.value) == "End line (2) must be greater than or equal to start line (3)"
|
|
90
|
+
|
|
91
|
+
async def test_read_file_section_start_line_exceeds_file_length(self, read_file_tool, sample_file):
|
|
92
|
+
with pytest.raises(ValueError) as exc_info:
|
|
93
|
+
await read_file_tool.read_file_section("test.txt", 6, 6)
|
|
94
|
+
assert str(exc_info.value) == "Start line 6 exceeds file length 5"
|
|
95
|
+
|
|
96
|
+
async def test_read_file_section_end_line_exceeds_file_length(self, read_file_tool, sample_file):
|
|
97
|
+
content = await read_file_tool.read_file_section("test.txt", 4, 10)
|
|
98
|
+
expected = "# test.txt (lines 4-5)\n\n```\nLine 4\nLine 5\n```"
|
|
99
|
+
assert content == expected
|
|
100
|
+
|
|
101
|
+
async def test_read_entire_file_truncation(self, read_file_tool, project_path):
|
|
102
|
+
"""Test that files over 2000 lines are truncated with a warning."""
|
|
103
|
+
# Create a large file with 2500 lines
|
|
104
|
+
large_file_path = project_path / "large_file.txt"
|
|
105
|
+
lines = [f"Line {i}\n" for i in range(1, 2501)]
|
|
106
|
+
large_file_path.write_text("".join(lines))
|
|
107
|
+
|
|
108
|
+
content = await read_file_tool.read_entire_file("large_file.txt")
|
|
109
|
+
|
|
110
|
+
# Check that the response indicates truncation
|
|
111
|
+
assert "# large_file.txt (TRUNCATED)" in content
|
|
112
|
+
assert "⚠️ File truncated: Showing first 2000 of 2500 lines" in content
|
|
113
|
+
assert "To read specific sections, use `read_file_section`" in content
|
|
114
|
+
|
|
115
|
+
# Verify that only 2000 lines are included
|
|
116
|
+
# Count the actual lines in the code block
|
|
117
|
+
code_block_start = content.find("```\n") + 4
|
|
118
|
+
code_block_end = content.rfind("\n```")
|
|
119
|
+
code_content = content[code_block_start:code_block_end]
|
|
120
|
+
actual_lines = code_content.strip().split("\n")
|
|
121
|
+
assert len(actual_lines) == 2000
|
|
122
|
+
assert actual_lines[0] == "Line 1"
|
|
123
|
+
assert actual_lines[-1] == "Line 2000"
|
|
124
|
+
|
|
125
|
+
async def test_read_entire_file_char_truncation(self, read_file_tool, project_path):
|
|
126
|
+
large_file_path = project_path / "large_one_line.html"
|
|
127
|
+
large_file_path.write_text("a" * 100_050)
|
|
128
|
+
|
|
129
|
+
content = await read_file_tool.read_entire_file("large_one_line.html")
|
|
130
|
+
|
|
131
|
+
assert "# large_one_line.html (TRUNCATED)" in content
|
|
132
|
+
assert "File truncated by size: Showing first 100,000 of 100,050 characters" in content
|
|
133
|
+
code_content = content.split("```\n", 1)[1].rsplit("\n```", 1)[0]
|
|
134
|
+
assert len(code_content) == 100_000
|
|
135
|
+
|
|
136
|
+
async def test_read_file_section_char_truncation(self, read_file_tool, project_path):
|
|
137
|
+
large_file_path = project_path / "large_section.txt"
|
|
138
|
+
large_file_path.write_text(("a" * 30_000 + "\n") * 5)
|
|
139
|
+
|
|
140
|
+
content = await read_file_tool.read_file_section("large_section.txt", 1, 5)
|
|
141
|
+
|
|
142
|
+
assert "# large_section.txt (lines 1-5) (TRUNCATED)" in content
|
|
143
|
+
assert "File truncated by size" in content
|
|
144
|
+
code_content = content.split("```\n", 1)[1].rsplit("\n```", 1)[0]
|
|
145
|
+
assert len(code_content) == 100_000
|
|
146
|
+
|
|
147
|
+
async def test_read_entire_file_exactly_at_limit(self, read_file_tool, project_path):
|
|
148
|
+
"""Test that files with exactly 2000 lines are not truncated."""
|
|
149
|
+
# Create a file with exactly 2000 lines
|
|
150
|
+
exact_limit_file_path = project_path / "exact_limit_file.txt"
|
|
151
|
+
lines = [f"Line {i}\n" for i in range(1, 2001)]
|
|
152
|
+
exact_limit_file_path.write_text("".join(lines))
|
|
153
|
+
|
|
154
|
+
content = await read_file_tool.read_entire_file("exact_limit_file.txt")
|
|
155
|
+
|
|
156
|
+
# Check that the response does NOT indicate truncation
|
|
157
|
+
assert "# exact_limit_file.txt\n\n```" in content
|
|
158
|
+
assert "(TRUNCATED)" not in content
|
|
159
|
+
assert "⚠️ File truncated" not in content
|
|
160
|
+
|
|
161
|
+
async def test_read_entire_file_below_limit(self, read_file_tool, project_path):
|
|
162
|
+
"""Test that files with fewer than 2000 lines are not truncated."""
|
|
163
|
+
# Create a file with 1999 lines
|
|
164
|
+
below_limit_file_path = project_path / "below_limit_file.txt"
|
|
165
|
+
lines = [f"Line {i}\n" for i in range(1, 2000)]
|
|
166
|
+
below_limit_file_path.write_text("".join(lines))
|
|
167
|
+
|
|
168
|
+
content = await read_file_tool.read_entire_file("below_limit_file.txt")
|
|
169
|
+
|
|
170
|
+
# Check that the response does NOT indicate truncation
|
|
171
|
+
assert "# below_limit_file.txt\n\n```" in content
|
|
172
|
+
assert "(TRUNCATED)" not in content
|
|
173
|
+
assert "⚠️ File truncated" not in content
|
|
@@ -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.replace_entire_file_tool import ReplaceEntireFileTool
|
|
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 replace_entire_file_tool(project_path, mock_connection_manager, agent_config, mock_base_agent):
|
|
44
|
+
return ReplaceEntireFileTool(
|
|
45
|
+
project_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def sample_file(project_path):
|
|
51
|
+
file_path = project_path / "test.txt"
|
|
52
|
+
file_path.write_text("Original content\nLine 2\nLine 3")
|
|
53
|
+
return file_path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
class TestReplaceEntireFileTool:
|
|
58
|
+
async def test_replace_entire_file_success(self, replace_entire_file_tool, sample_file):
|
|
59
|
+
new_content = "New content\nLine 2\nLine 3"
|
|
60
|
+
result = await replace_entire_file_tool.replace_entire_file("test.txt", new_content)
|
|
61
|
+
|
|
62
|
+
assert result == "# test.txt has been replaced."
|
|
63
|
+
assert sample_file.read_text() == new_content
|
|
64
|
+
|
|
65
|
+
async def test_replace_entire_file_with_empty_content(self, replace_entire_file_tool, sample_file):
|
|
66
|
+
result = await replace_entire_file_tool.replace_entire_file("test.txt", "")
|
|
67
|
+
|
|
68
|
+
assert result == "# test.txt has been replaced."
|
|
69
|
+
assert sample_file.read_text() == ""
|
|
70
|
+
|
|
71
|
+
async def test_replace_entire_file_with_multiline_content(self, replace_entire_file_tool, sample_file):
|
|
72
|
+
new_content = "Line 1\n\nLine 3\n\nLine 5"
|
|
73
|
+
result = await replace_entire_file_tool.replace_entire_file("test.txt", new_content)
|
|
74
|
+
|
|
75
|
+
assert result == "# test.txt has been replaced."
|
|
76
|
+
assert sample_file.read_text() == new_content
|
|
77
|
+
|
|
78
|
+
async def test_replace_entire_file_file_not_found(self, replace_entire_file_tool):
|
|
79
|
+
with pytest.raises(FileNotFoundError) as exc_info:
|
|
80
|
+
await replace_entire_file_tool.replace_entire_file("nonexistent.txt", "Content")
|
|
81
|
+
assert str(exc_info.value) == "File not found: nonexistent.txt"
|
|
82
|
+
|
|
83
|
+
@patch("pathlib.Path.write_text")
|
|
84
|
+
async def test_replace_entire_file_permission_error(self, mock_write_text, replace_entire_file_tool, sample_file):
|
|
85
|
+
mock_write_text.side_effect = PermissionError("Permission denied")
|
|
86
|
+
|
|
87
|
+
with pytest.raises(PermissionError) as exc_info:
|
|
88
|
+
await replace_entire_file_tool.replace_entire_file("test.txt", "Content")
|
|
89
|
+
assert str(exc_info.value) == "Permission denied"
|
|
90
|
+
|
|
91
|
+
@patch("pathlib.Path.write_text")
|
|
92
|
+
async def test_replace_entire_file_general_error(self, mock_write_text, replace_entire_file_tool, sample_file):
|
|
93
|
+
mock_write_text.side_effect = Exception("Unexpected error")
|
|
94
|
+
|
|
95
|
+
with pytest.raises(Exception) as exc_info:
|
|
96
|
+
await replace_entire_file_tool.replace_entire_file("test.txt", "Content")
|
|
97
|
+
assert str(exc_info.value) == "Unexpected error"
|
|
98
|
+
|
|
99
|
+
async def test_replace_entire_file_preserve_newline(self, replace_entire_file_tool, project_path):
|
|
100
|
+
# Create a file with a trailing newline
|
|
101
|
+
file_path = project_path / "newline.txt"
|
|
102
|
+
file_path.write_text("Line 1\nLine 2\n")
|
|
103
|
+
|
|
104
|
+
new_content = "New Line 1\nNew Line 2\n"
|
|
105
|
+
result = await replace_entire_file_tool.replace_entire_file("newline.txt", new_content)
|
|
106
|
+
|
|
107
|
+
assert result == "# newline.txt has been replaced."
|
|
108
|
+
assert file_path.read_text() == new_content
|
|
109
|
+
|
|
110
|
+
async def test_replace_entire_file_with_special_characters(self, replace_entire_file_tool, sample_file):
|
|
111
|
+
new_content = "Special chars: !@#$%^&*()\nUnicode: 🚀\nTabs:\t\t\t\n"
|
|
112
|
+
result = await replace_entire_file_tool.replace_entire_file("test.txt", new_content)
|
|
113
|
+
|
|
114
|
+
assert result == "# test.txt has been replaced."
|
|
115
|
+
assert sample_file.read_text() == new_content
|