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,196 @@
|
|
|
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.glob_tool import GlobTool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_connection_manager():
|
|
12
|
+
mock = AsyncMock()
|
|
13
|
+
return mock
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def project_path(tmp_path):
|
|
18
|
+
return tmp_path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def agent_config():
|
|
23
|
+
return AgentConfig(
|
|
24
|
+
anthropic_api_key="test_key",
|
|
25
|
+
openai_api_key="test-key",
|
|
26
|
+
long_context_config=ModelConfig(
|
|
27
|
+
provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()
|
|
28
|
+
),
|
|
29
|
+
fast_config=ModelConfig(provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()),
|
|
30
|
+
thinking_config=ModelConfig(
|
|
31
|
+
provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig(), thinking_tokens=1024
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def mock_base_agent():
|
|
38
|
+
mock = Mock()
|
|
39
|
+
mock.agent_name = "test_agent"
|
|
40
|
+
return mock
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def glob_tool(project_path, mock_connection_manager, agent_config, mock_base_agent):
|
|
45
|
+
return GlobTool(
|
|
46
|
+
project_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def sample_files(project_path):
|
|
52
|
+
# Create a directory structure with various files
|
|
53
|
+
(project_path / "src").mkdir()
|
|
54
|
+
(project_path / "tests").mkdir()
|
|
55
|
+
(project_path / "docs").mkdir()
|
|
56
|
+
|
|
57
|
+
# Create Python files
|
|
58
|
+
(project_path / "src" / "main.py").write_text('def main():\n print("Hello World")\n')
|
|
59
|
+
(project_path / "src" / "utils.py").write_text('def helper():\n return "Helper function"\n')
|
|
60
|
+
(project_path / "tests" / "test_main.py").write_text("def test_main():\n assert True\n")
|
|
61
|
+
|
|
62
|
+
# Create other file types
|
|
63
|
+
(project_path / "docs" / "README.md").write_text("# Project Documentation")
|
|
64
|
+
(project_path / "docs" / "CHANGELOG.md").write_text("# Changelog")
|
|
65
|
+
|
|
66
|
+
# Create a binary file
|
|
67
|
+
(project_path / "src" / "data.bin").write_bytes(b"\x00\x01\x02\x03")
|
|
68
|
+
|
|
69
|
+
# Create a file in excluded directory
|
|
70
|
+
(project_path / ".git").mkdir()
|
|
71
|
+
(project_path / ".git" / "config").write_text("git config content")
|
|
72
|
+
|
|
73
|
+
# Create a large file
|
|
74
|
+
(project_path / "src" / "large.txt").write_text("x" * (11 * 1024 * 1024))
|
|
75
|
+
|
|
76
|
+
return project_path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestGlobTool:
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
async def test_find_files_by_pattern_basic(self, glob_tool, sample_files):
|
|
82
|
+
result = await glob_tool.find_files_by_pattern("**/*.py")
|
|
83
|
+
|
|
84
|
+
assert "# Files Matching '**/*.py'" in result
|
|
85
|
+
assert "Found 3 matching items" in result
|
|
86
|
+
assert "**main.py**" in result
|
|
87
|
+
assert "**utils.py**" in result
|
|
88
|
+
assert "**test_main.py**" in result
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_find_files_by_pattern_recursive(self, glob_tool, sample_files):
|
|
92
|
+
result = await glob_tool.find_files_by_pattern("**/*.md")
|
|
93
|
+
|
|
94
|
+
assert "# Files Matching '**/*.md'" in result
|
|
95
|
+
assert "Found 2 matching items" in result
|
|
96
|
+
assert "**README.md**" in result
|
|
97
|
+
assert "**CHANGELOG.md**" in result
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_find_files_by_pattern_no_matches(self, glob_tool, sample_files):
|
|
101
|
+
result = await glob_tool.find_files_by_pattern("*.nonexistent")
|
|
102
|
+
|
|
103
|
+
assert "No files found matching pattern: '*.nonexistent'" in result
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_find_files_by_pattern_include_directories(self, glob_tool, sample_files):
|
|
107
|
+
result = await glob_tool.find_files_by_pattern("*", include_directories=True)
|
|
108
|
+
|
|
109
|
+
assert "📁 Directory" in result
|
|
110
|
+
assert "**src**" in result
|
|
111
|
+
assert "**tests**" in result
|
|
112
|
+
assert "**docs**" in result
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio
|
|
115
|
+
async def test_find_files_by_pattern_exclude_directories(self, glob_tool, sample_files):
|
|
116
|
+
result = await glob_tool.find_files_by_pattern("**/*", include_directories=False)
|
|
117
|
+
|
|
118
|
+
assert "📁 Directory" not in result
|
|
119
|
+
assert "**main.py**" in result
|
|
120
|
+
assert "**utils.py**" in result
|
|
121
|
+
assert "**test_main.py**" in result
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_find_files_by_pattern_without_details(self, glob_tool, sample_files):
|
|
125
|
+
result = await glob_tool.find_files_by_pattern("**/*.py", show_details=False)
|
|
126
|
+
|
|
127
|
+
assert "**main.py**" in result
|
|
128
|
+
assert "**utils.py**" in result
|
|
129
|
+
assert "**test_main.py**" in result
|
|
130
|
+
assert "Size:" not in result
|
|
131
|
+
assert "Modified:" not in result
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_find_files_by_pattern_excludes_binary_files(self, glob_tool, sample_files):
|
|
135
|
+
result = await glob_tool.find_files_by_pattern("**/*")
|
|
136
|
+
|
|
137
|
+
assert "**data.bin**" not in result
|
|
138
|
+
|
|
139
|
+
@pytest.mark.asyncio
|
|
140
|
+
async def test_find_files_by_pattern_excludes_git_files(self, glob_tool, sample_files):
|
|
141
|
+
result = await glob_tool.find_files_by_pattern("**/*")
|
|
142
|
+
|
|
143
|
+
assert ".git/config" not in result
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_find_files_by_pattern_excludes_large_files(self, glob_tool, sample_files):
|
|
147
|
+
result = await glob_tool.find_files_by_pattern("**/*")
|
|
148
|
+
|
|
149
|
+
assert "**large.txt**" not in result
|
|
150
|
+
|
|
151
|
+
@pytest.mark.asyncio
|
|
152
|
+
async def test_find_files_by_pattern_result_limit(self, glob_tool, sample_files):
|
|
153
|
+
# Create many files
|
|
154
|
+
for i in range(150):
|
|
155
|
+
(sample_files / f"file_{i}.txt").write_text("test")
|
|
156
|
+
|
|
157
|
+
result = await glob_tool.find_files_by_pattern("*.txt")
|
|
158
|
+
|
|
159
|
+
assert "Found 150 matching items" in result
|
|
160
|
+
assert "showing first 128" in result
|
|
161
|
+
|
|
162
|
+
@pytest.mark.asyncio
|
|
163
|
+
async def test_find_files_by_pattern_grouped_by_directory(self, glob_tool, sample_files):
|
|
164
|
+
result = await glob_tool.find_files_by_pattern("**/*.py")
|
|
165
|
+
|
|
166
|
+
assert "# Files Matching '**/*.py'" in result
|
|
167
|
+
assert "## src/" in result
|
|
168
|
+
assert "## tests/" in result
|
|
169
|
+
assert "**main.py**" in result
|
|
170
|
+
assert "**utils.py**" in result
|
|
171
|
+
assert "**test_main.py**" in result
|
|
172
|
+
|
|
173
|
+
@pytest.mark.asyncio
|
|
174
|
+
async def test_find_files_by_pattern_in_directory(self, glob_tool, sample_files):
|
|
175
|
+
result = await glob_tool.find_files_by_pattern("src/*.py")
|
|
176
|
+
|
|
177
|
+
assert "**main.py**" in result
|
|
178
|
+
assert "**utils.py**" in result
|
|
179
|
+
assert "**test_main.py**" not in result
|
|
180
|
+
|
|
181
|
+
@pytest.mark.asyncio
|
|
182
|
+
async def test_find_files_by_pattern_with_special_characters(self, glob_tool, sample_files):
|
|
183
|
+
# Create a file with special characters
|
|
184
|
+
special_file = sample_files / "src" / "special@#$%.txt"
|
|
185
|
+
special_file.write_text("test")
|
|
186
|
+
|
|
187
|
+
result = await glob_tool.find_files_by_pattern("**/*.txt")
|
|
188
|
+
|
|
189
|
+
assert "**special@#$%.txt**" in result
|
|
190
|
+
|
|
191
|
+
@pytest.mark.asyncio
|
|
192
|
+
async def test_find_files_by_pattern_leading_slash(self, glob_tool, sample_files):
|
|
193
|
+
result = await glob_tool.find_files_by_pattern("/src/*.py")
|
|
194
|
+
|
|
195
|
+
assert "**main.py**" in result
|
|
196
|
+
assert "**utils.py**" in result
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, List
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import uuid
|
|
8
|
+
from unittest.mock import AsyncMock, Mock
|
|
9
|
+
|
|
10
|
+
from kolega_code.config import AgentConfig, ModelConfig, ModelProvider, RateLimitConfig
|
|
11
|
+
from kolega_code.agent.tool_backend.glob_tool import GlobTool
|
|
12
|
+
from kolega_code.services.file_system import LocalFileSystem
|
|
13
|
+
from kolega_code.sandbox.filesystem import SandboxFileSystem
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def agent_config():
|
|
18
|
+
return AgentConfig(
|
|
19
|
+
anthropic_api_key="test_key",
|
|
20
|
+
openai_api_key="test-key",
|
|
21
|
+
long_context_config=ModelConfig(
|
|
22
|
+
provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()
|
|
23
|
+
),
|
|
24
|
+
fast_config=ModelConfig(provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()),
|
|
25
|
+
thinking_config=ModelConfig(
|
|
26
|
+
provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig(), thinking_tokens=1024
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mock_base_agent():
|
|
33
|
+
mock = Mock()
|
|
34
|
+
mock.agent_name = "test_agent"
|
|
35
|
+
return mock
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def mock_connection_manager():
|
|
40
|
+
return AsyncMock()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def sample_project(tmp_path: Path):
|
|
45
|
+
# Directories
|
|
46
|
+
(tmp_path / "src").mkdir()
|
|
47
|
+
(tmp_path / "tests").mkdir()
|
|
48
|
+
(tmp_path / "docs").mkdir()
|
|
49
|
+
|
|
50
|
+
# Files
|
|
51
|
+
(tmp_path / "src" / "main.py").write_text("print('Hello World')\n")
|
|
52
|
+
(tmp_path / "src" / "utils.py").write_text("def helper():\n return 1\n")
|
|
53
|
+
(tmp_path / "tests" / "test_main.py").write_text("def test_main():\n assert True\n")
|
|
54
|
+
(tmp_path / "docs" / "README.md").write_text("# Readme")
|
|
55
|
+
|
|
56
|
+
# Excluded dir content
|
|
57
|
+
(tmp_path / ".git").mkdir()
|
|
58
|
+
(tmp_path / ".git" / "config").write_text("git")
|
|
59
|
+
|
|
60
|
+
# Binary and large files
|
|
61
|
+
(tmp_path / "src" / "data.bin").write_bytes(b"\x00\x01\x02")
|
|
62
|
+
(tmp_path / "src" / "large.txt").write_text("x" * (11 * 1024 * 1024))
|
|
63
|
+
|
|
64
|
+
return tmp_path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_embedded_var(script: str, var_name: str, default: str = "") -> str:
|
|
68
|
+
prefix = f"{var_name}="
|
|
69
|
+
for line in script.splitlines():
|
|
70
|
+
if line.strip().startswith(prefix):
|
|
71
|
+
return line.split("=", 1)[1].strip().strip("'\"")
|
|
72
|
+
return default
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _glob_from_root(root: Path, pattern: str) -> List[str]:
|
|
76
|
+
# Use Python glob relative to root, return relative paths
|
|
77
|
+
matches = list(root.glob(pattern)) if "**" not in pattern else list(root.rglob(pattern.split("**/", 1)[1]))
|
|
78
|
+
# For generality, fall back to glob.glob with recursive
|
|
79
|
+
if not matches:
|
|
80
|
+
matches = list(root.glob(pattern))
|
|
81
|
+
rels: List[str] = []
|
|
82
|
+
for p in matches:
|
|
83
|
+
try:
|
|
84
|
+
rel = str(p.relative_to(root))
|
|
85
|
+
except Exception:
|
|
86
|
+
rel = str(p)
|
|
87
|
+
rels.append(rel)
|
|
88
|
+
# Deduplicate and sort
|
|
89
|
+
return sorted(dict.fromkeys(rels).keys())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _make_mock_sandbox_for_glob(tmp_path: Path) -> Any:
|
|
93
|
+
sandbox = Mock()
|
|
94
|
+
sandbox.files = Mock()
|
|
95
|
+
sandbox.commands = Mock()
|
|
96
|
+
|
|
97
|
+
exclude_dirs = GlobTool.EXCLUDE_DIRS
|
|
98
|
+
bin_exts = GlobTool.BINARY_EXTENSIONS
|
|
99
|
+
|
|
100
|
+
def run_side_effect(cmd: str):
|
|
101
|
+
result = Mock()
|
|
102
|
+
result.exit_code = 0
|
|
103
|
+
result.stderr = ""
|
|
104
|
+
result.stdout = ""
|
|
105
|
+
|
|
106
|
+
if "bash -O globstar" in cmd:
|
|
107
|
+
pattern = _parse_embedded_var(cmd, "pattern", "")
|
|
108
|
+
include_dirs_flag = _parse_embedded_var(cmd, "include_dirs", "0")
|
|
109
|
+
include_dirs = include_dirs_flag == "1"
|
|
110
|
+
|
|
111
|
+
# Compute matches using Python glob
|
|
112
|
+
matches = _glob_from_root(tmp_path, pattern)
|
|
113
|
+
|
|
114
|
+
# Apply excludes and build rows
|
|
115
|
+
rows: List[str] = []
|
|
116
|
+
total = 0
|
|
117
|
+
for rel in matches:
|
|
118
|
+
parts = Path(rel).parts
|
|
119
|
+
if any(part in exclude_dirs for part in parts):
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
full = tmp_path / rel
|
|
123
|
+
if full.is_file():
|
|
124
|
+
# exclude by extension and size
|
|
125
|
+
if full.suffix.lower() in bin_exts:
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
size = full.stat().st_size
|
|
129
|
+
if size > GlobTool.MAX_FILE_SIZE_BYTES:
|
|
130
|
+
continue
|
|
131
|
+
mtime = int(full.stat().st_mtime)
|
|
132
|
+
except Exception:
|
|
133
|
+
continue
|
|
134
|
+
total += 1
|
|
135
|
+
if len(rows) < GlobTool.MAX_RESULTS:
|
|
136
|
+
rows.append(f"{rel}\tf\t{size}\t{mtime}")
|
|
137
|
+
elif full.is_dir() and include_dirs:
|
|
138
|
+
try:
|
|
139
|
+
mtime = int(full.stat().st_mtime)
|
|
140
|
+
except Exception:
|
|
141
|
+
mtime = 0
|
|
142
|
+
total += 1
|
|
143
|
+
if len(rows) < GlobTool.MAX_RESULTS:
|
|
144
|
+
rows.append(f"{rel}\td\t0\t{mtime}")
|
|
145
|
+
|
|
146
|
+
# Compose stdout
|
|
147
|
+
out = "\n".join(rows) + f"\n__TOTAL__ {total}\n"
|
|
148
|
+
result.stdout = out
|
|
149
|
+
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
from unittest.mock import AsyncMock as _AsyncMock
|
|
153
|
+
|
|
154
|
+
sandbox.commands.run = _AsyncMock(side_effect=run_side_effect)
|
|
155
|
+
sandbox.files.read = lambda p: (tmp_path / p).read_text() if (tmp_path / p).exists() else ""
|
|
156
|
+
|
|
157
|
+
return sandbox
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.asyncio
|
|
161
|
+
async def test_sandbox_fast_path_detection(sample_project, mock_connection_manager, agent_config, mock_base_agent):
|
|
162
|
+
mock_sandbox = _make_mock_sandbox_for_glob(sample_project)
|
|
163
|
+
sandbox_fs = SandboxFileSystem(mock_sandbox, str(sample_project))
|
|
164
|
+
|
|
165
|
+
tool = GlobTool(
|
|
166
|
+
sample_project,
|
|
167
|
+
"test_workspace",
|
|
168
|
+
str(uuid.uuid4()),
|
|
169
|
+
mock_connection_manager,
|
|
170
|
+
agent_config,
|
|
171
|
+
mock_base_agent,
|
|
172
|
+
filesystem=sandbox_fs,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
result = await tool.find_files_by_pattern("**/*.py")
|
|
176
|
+
# Ensure bash path invoked
|
|
177
|
+
assert any("bash -O globstar" in str(call) for call in mock_sandbox.commands.run.call_args_list)
|
|
178
|
+
assert "**main.py**" in result
|
|
179
|
+
assert "**utils.py**" in result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.asyncio
|
|
183
|
+
async def test_glob_tool_local_vs_sandbox_parity(
|
|
184
|
+
sample_project, mock_connection_manager, agent_config, mock_base_agent
|
|
185
|
+
):
|
|
186
|
+
# Local tool
|
|
187
|
+
local_tool = GlobTool(
|
|
188
|
+
sample_project,
|
|
189
|
+
"test_workspace",
|
|
190
|
+
str(uuid.uuid4()),
|
|
191
|
+
mock_connection_manager,
|
|
192
|
+
agent_config,
|
|
193
|
+
mock_base_agent,
|
|
194
|
+
filesystem=LocalFileSystem(root_path=sample_project),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Sandbox tool with mock sandbox
|
|
198
|
+
mock_sandbox = _make_mock_sandbox_for_glob(sample_project)
|
|
199
|
+
sandbox_fs = SandboxFileSystem(mock_sandbox, str(sample_project))
|
|
200
|
+
sandbox_tool = GlobTool(
|
|
201
|
+
sample_project,
|
|
202
|
+
"test_workspace",
|
|
203
|
+
str(uuid.uuid4()),
|
|
204
|
+
mock_connection_manager,
|
|
205
|
+
agent_config,
|
|
206
|
+
mock_base_agent,
|
|
207
|
+
filesystem=sandbox_fs,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
patterns = ["**/*.py", "src/*.py", "**/*.md", "*.md"]
|
|
211
|
+
for pat in patterns:
|
|
212
|
+
local_res = await local_tool.find_files_by_pattern(pat)
|
|
213
|
+
sandbox_res = await sandbox_tool.find_files_by_pattern(pat)
|
|
214
|
+
|
|
215
|
+
# Normalize outcome parity
|
|
216
|
+
local_has = "# Files Matching" in local_res
|
|
217
|
+
sandbox_has = "# Files Matching" in sandbox_res
|
|
218
|
+
assert (
|
|
219
|
+
local_has == sandbox_has
|
|
220
|
+
), f"Mismatch in results presence for pattern {pat}:\nlocal={local_res}\nsandbox={sandbox_res}"
|
|
221
|
+
|
|
222
|
+
# Compare presence of key filenames
|
|
223
|
+
for fname in ["main.py", "utils.py", "test_main.py", "README.md"]:
|
|
224
|
+
assert (fname in local_res) == (fname in sandbox_res), f"Mismatch for {fname} with pattern {pat}"
|
|
225
|
+
|
|
226
|
+
# Include directories
|
|
227
|
+
local_res_dirs = await local_tool.find_files_by_pattern("*", include_directories=True)
|
|
228
|
+
sandbox_res_dirs = await sandbox_tool.find_files_by_pattern("*", include_directories=True)
|
|
229
|
+
for d in ["src", "tests", "docs"]:
|
|
230
|
+
assert (d in local_res_dirs) == (d in sandbox_res_dirs), f"Directory presence mismatch for {d}"
|