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