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