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,482 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from kolega_code.config import AgentConfig, ModelConfig, ModelProvider, RateLimitConfig
|
|
9
|
+
from kolega_code.events import AgentEvent
|
|
10
|
+
from kolega_code.agent.tool_backend.terminal_tool import TerminalTool
|
|
11
|
+
|
|
12
|
+
# Check if running in CI environment
|
|
13
|
+
SKIP_IN_CI = bool(os.getenv("CI")) or bool(os.getenv("GITLAB_CI"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_connection_manager():
|
|
18
|
+
return AsyncMock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def project_path(tmp_path):
|
|
23
|
+
return tmp_path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def agent_config():
|
|
28
|
+
return AgentConfig(
|
|
29
|
+
anthropic_api_key="test_key",
|
|
30
|
+
openai_api_key="test_key",
|
|
31
|
+
long_context_config=ModelConfig(
|
|
32
|
+
provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()
|
|
33
|
+
),
|
|
34
|
+
fast_config=ModelConfig(provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig()),
|
|
35
|
+
thinking_config=ModelConfig(
|
|
36
|
+
provider=ModelProvider.ANTHROPIC, model="test-model", rate_limits=RateLimitConfig(), thinking_tokens=1024
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def mock_base_agent():
|
|
43
|
+
mock = Mock()
|
|
44
|
+
mock.agent_name = "test_agent"
|
|
45
|
+
return mock
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def terminal_tool(project_path, mock_connection_manager, agent_config, mock_base_agent):
|
|
50
|
+
tool = TerminalTool(
|
|
51
|
+
project_path, "test_workspace", str(uuid.uuid4()), mock_connection_manager, agent_config, mock_base_agent
|
|
52
|
+
)
|
|
53
|
+
# Set initialized=True to prevent auto-initialization during tests
|
|
54
|
+
tool.initialized = True
|
|
55
|
+
return tool
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestTerminalTool:
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_execute_terminal_command_success(self, terminal_tool, mock_connection_manager):
|
|
61
|
+
# Mock the subprocess
|
|
62
|
+
mock_process = AsyncMock()
|
|
63
|
+
mock_process.communicate = AsyncMock(return_value=(b"stdout", b""))
|
|
64
|
+
|
|
65
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_process):
|
|
66
|
+
result = await terminal_tool.execute_terminal_command('echo "Hello World"')
|
|
67
|
+
|
|
68
|
+
assert result == "stdout"
|
|
69
|
+
|
|
70
|
+
# Verify connection manager was called correctly
|
|
71
|
+
mock_connection_manager.broadcast_event.assert_called()
|
|
72
|
+
calls = mock_connection_manager.broadcast_event.call_args_list
|
|
73
|
+
|
|
74
|
+
# Check log message broadcast (first call)
|
|
75
|
+
assert isinstance(calls[0][0][0], AgentEvent)
|
|
76
|
+
assert calls[0][0][0].event_type == "log_message"
|
|
77
|
+
assert calls[0][0][0].content["text"] == 'Executing command: echo "Hello World"'
|
|
78
|
+
assert calls[0][0][0].content["level"] == "info"
|
|
79
|
+
assert calls[0][0][1] == "test_workspace"
|
|
80
|
+
|
|
81
|
+
# Check command broadcast (second call)
|
|
82
|
+
assert isinstance(calls[1][0][0], AgentEvent)
|
|
83
|
+
assert calls[1][0][0].event_type == "terminal_command"
|
|
84
|
+
assert calls[1][0][0].content["command"] == 'echo "Hello World"'
|
|
85
|
+
assert calls[1][0][1] == "test_workspace"
|
|
86
|
+
|
|
87
|
+
# Check output broadcast (third call)
|
|
88
|
+
assert isinstance(calls[2][0][0], AgentEvent)
|
|
89
|
+
assert calls[2][0][0].event_type == "terminal_output"
|
|
90
|
+
assert calls[2][0][0].content["output"] == "stdout"
|
|
91
|
+
assert calls[2][0][1] == "test_workspace"
|
|
92
|
+
|
|
93
|
+
@pytest.mark.asyncio
|
|
94
|
+
async def test_execute_terminal_command_with_stderr(self, terminal_tool, mock_connection_manager):
|
|
95
|
+
# Mock the subprocess
|
|
96
|
+
mock_process = AsyncMock()
|
|
97
|
+
mock_process.communicate = AsyncMock(return_value=(b"stdout", b"stderr"))
|
|
98
|
+
|
|
99
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_process):
|
|
100
|
+
result = await terminal_tool.execute_terminal_command("test command")
|
|
101
|
+
|
|
102
|
+
assert result == "stdoutstderr"
|
|
103
|
+
|
|
104
|
+
@pytest.mark.asyncio
|
|
105
|
+
async def test_execute_terminal_command_timeout(self, terminal_tool, mock_connection_manager):
|
|
106
|
+
# Mock the subprocess to simulate a timeout
|
|
107
|
+
mock_process = AsyncMock()
|
|
108
|
+
mock_process.communicate = AsyncMock(side_effect=asyncio.TimeoutError())
|
|
109
|
+
mock_process.terminate = Mock()
|
|
110
|
+
|
|
111
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_process):
|
|
112
|
+
result = await terminal_tool.execute_terminal_command("slow command")
|
|
113
|
+
|
|
114
|
+
assert result == "Command timed out after 15 seconds"
|
|
115
|
+
mock_process.terminate.assert_called_once()
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_execute_terminal_command_process_error(self, terminal_tool, mock_connection_manager):
|
|
119
|
+
# Mock the subprocess to raise an exception
|
|
120
|
+
mock_process = AsyncMock()
|
|
121
|
+
mock_process.communicate = AsyncMock(side_effect=Exception("Process error"))
|
|
122
|
+
|
|
123
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_process):
|
|
124
|
+
result = await terminal_tool.execute_terminal_command("error command")
|
|
125
|
+
|
|
126
|
+
assert "Command execution failed: Process error" in result
|
|
127
|
+
|
|
128
|
+
@pytest.mark.asyncio
|
|
129
|
+
async def test_execute_terminal_command_terminate_error(self, terminal_tool, mock_connection_manager):
|
|
130
|
+
# Mock the subprocess to simulate a timeout and raise error on terminate
|
|
131
|
+
mock_process = AsyncMock()
|
|
132
|
+
mock_process.communicate = AsyncMock(side_effect=asyncio.TimeoutError())
|
|
133
|
+
mock_process.terminate = Mock(side_effect=Exception("Terminate error"))
|
|
134
|
+
|
|
135
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_process):
|
|
136
|
+
result = await terminal_tool.execute_terminal_command("slow command")
|
|
137
|
+
|
|
138
|
+
assert result == "Command timed out after 15 seconds"
|
|
139
|
+
mock_process.terminate.assert_called_once()
|
|
140
|
+
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_execute_terminal_command_empty_output(self, terminal_tool, mock_connection_manager):
|
|
143
|
+
# Mock the subprocess with empty output
|
|
144
|
+
mock_process = AsyncMock()
|
|
145
|
+
mock_process.communicate = AsyncMock(return_value=(b"", b""))
|
|
146
|
+
|
|
147
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_process):
|
|
148
|
+
result = await terminal_tool.execute_terminal_command("empty command")
|
|
149
|
+
|
|
150
|
+
assert result == ""
|
|
151
|
+
|
|
152
|
+
@pytest.mark.asyncio
|
|
153
|
+
async def test_execute_terminal_command_unicode_output(self, terminal_tool, mock_connection_manager):
|
|
154
|
+
# Mock the subprocess with unicode output
|
|
155
|
+
mock_process = AsyncMock()
|
|
156
|
+
mock_process.communicate = AsyncMock(return_value=(b"Hello \xe2\x9c\xa8", b""))
|
|
157
|
+
|
|
158
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_process):
|
|
159
|
+
result = await terminal_tool.execute_terminal_command("unicode command")
|
|
160
|
+
|
|
161
|
+
assert result == "Hello ✨"
|
|
162
|
+
|
|
163
|
+
@pytest.mark.asyncio
|
|
164
|
+
async def test_execute_terminal_command_working_directory(self, terminal_tool, mock_connection_manager):
|
|
165
|
+
# Mock the subprocess
|
|
166
|
+
mock_process = AsyncMock()
|
|
167
|
+
mock_process.communicate = AsyncMock(return_value=(b"stdout", b""))
|
|
168
|
+
|
|
169
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_process) as mock_create:
|
|
170
|
+
await terminal_tool.execute_terminal_command("pwd")
|
|
171
|
+
|
|
172
|
+
# Verify the command was executed in the correct directory
|
|
173
|
+
mock_create.assert_called_once()
|
|
174
|
+
assert mock_create.call_args[1]["cwd"] == str(terminal_tool.project_path)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestTerminalToolCommandTracking:
|
|
178
|
+
"""Tests for the command tracking functionality"""
|
|
179
|
+
|
|
180
|
+
@pytest.mark.asyncio
|
|
181
|
+
async def test_run_command_tracked_success(self, terminal_tool, mock_connection_manager):
|
|
182
|
+
# Mock the terminal manager
|
|
183
|
+
mock_terminal_manager = AsyncMock()
|
|
184
|
+
mock_terminal_manager.send_command_tracked = AsyncMock(return_value="terminal_1_1")
|
|
185
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
186
|
+
|
|
187
|
+
result = await terminal_tool.run_command_tracked("terminal_1", "echo test", "Test command")
|
|
188
|
+
|
|
189
|
+
assert result == "terminal_1_1"
|
|
190
|
+
mock_terminal_manager.send_command_tracked.assert_called_once_with(
|
|
191
|
+
"terminal_1", "echo test", "Test command", timeout=0
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@pytest.mark.asyncio
|
|
195
|
+
async def test_run_command_tracked_failure(self, terminal_tool, mock_connection_manager):
|
|
196
|
+
# Mock the terminal manager to return None (failure)
|
|
197
|
+
mock_terminal_manager = AsyncMock()
|
|
198
|
+
mock_terminal_manager.send_command_tracked = AsyncMock(return_value=None)
|
|
199
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
200
|
+
|
|
201
|
+
result = await terminal_tool.run_command_tracked("terminal_1", "echo test", "Test command")
|
|
202
|
+
|
|
203
|
+
assert "Failed to start command `echo test` in terminal terminal_1" in result
|
|
204
|
+
|
|
205
|
+
@pytest.mark.asyncio
|
|
206
|
+
async def test_run_command_tracked_terminal_not_found(self, terminal_tool, mock_connection_manager):
|
|
207
|
+
# Mock the terminal manager to raise KeyError
|
|
208
|
+
mock_terminal_manager = AsyncMock()
|
|
209
|
+
mock_terminal_manager.send_command_tracked = AsyncMock(
|
|
210
|
+
side_effect=KeyError("Terminal with ID invalid not found")
|
|
211
|
+
)
|
|
212
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
213
|
+
|
|
214
|
+
result = await terminal_tool.run_command_tracked("invalid", "echo test", "Test command")
|
|
215
|
+
|
|
216
|
+
assert "Terminal with ID invalid not found" in result
|
|
217
|
+
|
|
218
|
+
@pytest.mark.asyncio
|
|
219
|
+
async def test_send_terminal_input_success(self, terminal_tool, mock_connection_manager):
|
|
220
|
+
mock_terminal_manager = AsyncMock()
|
|
221
|
+
mock_terminal_manager.send_input = AsyncMock(return_value=True)
|
|
222
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
223
|
+
|
|
224
|
+
result = await terminal_tool.send_terminal_input("terminal_1", "Ada", submit=True, command_id="cmd_1")
|
|
225
|
+
|
|
226
|
+
assert result == "Sent input to terminal terminal_1 for command cmd_1 and submitted it."
|
|
227
|
+
assert "Ada" not in result
|
|
228
|
+
mock_terminal_manager.send_input.assert_awaited_once_with(
|
|
229
|
+
"terminal_1", "Ada", submit=True, command_id="cmd_1"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@pytest.mark.asyncio
|
|
233
|
+
async def test_send_terminal_input_returns_readable_value_error(self, terminal_tool, mock_connection_manager):
|
|
234
|
+
mock_terminal_manager = AsyncMock()
|
|
235
|
+
mock_terminal_manager.send_input = AsyncMock(side_effect=ValueError("No active command is running"))
|
|
236
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
237
|
+
|
|
238
|
+
result = await terminal_tool.send_terminal_input("terminal_1", "Ada")
|
|
239
|
+
|
|
240
|
+
assert result == "No active command is running"
|
|
241
|
+
|
|
242
|
+
@pytest.mark.asyncio
|
|
243
|
+
async def test_check_command_status_running(self, terminal_tool, mock_connection_manager):
|
|
244
|
+
# Mock the terminal manager
|
|
245
|
+
mock_terminal_manager = Mock()
|
|
246
|
+
mock_terminal_manager.get_command_status = Mock(
|
|
247
|
+
return_value={"status": "running", "command": "sleep 10", "duration": 5.2, "child_pids": [1234, 5678]}
|
|
248
|
+
)
|
|
249
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
250
|
+
|
|
251
|
+
result = await terminal_tool.check_command_status("terminal_1", "cmd_1")
|
|
252
|
+
|
|
253
|
+
assert "🔄 Command still running in terminal terminal_1 after 5.2s (2 child processes)" in result
|
|
254
|
+
assert "Command: sleep 10" in result
|
|
255
|
+
|
|
256
|
+
@pytest.mark.asyncio
|
|
257
|
+
async def test_check_command_status_completed(self, terminal_tool, mock_connection_manager):
|
|
258
|
+
# Mock the terminal manager
|
|
259
|
+
mock_terminal_manager = Mock()
|
|
260
|
+
mock_terminal_manager.get_command_status = Mock(
|
|
261
|
+
return_value={
|
|
262
|
+
"status": "completed",
|
|
263
|
+
"command": "echo test",
|
|
264
|
+
"duration": 1.5,
|
|
265
|
+
"return_code": 0,
|
|
266
|
+
"child_pids": [],
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
270
|
+
|
|
271
|
+
result = await terminal_tool.check_command_status("terminal_1", "cmd_1")
|
|
272
|
+
|
|
273
|
+
assert "✅ Command completed in 1.5s with exit code 0" in result
|
|
274
|
+
assert "Command: echo test" in result
|
|
275
|
+
|
|
276
|
+
@pytest.mark.asyncio
|
|
277
|
+
async def test_check_command_status_terminated(self, terminal_tool, mock_connection_manager):
|
|
278
|
+
# Mock the terminal manager
|
|
279
|
+
mock_terminal_manager = Mock()
|
|
280
|
+
mock_terminal_manager.get_command_status = Mock(
|
|
281
|
+
return_value={
|
|
282
|
+
"status": "terminated",
|
|
283
|
+
"command": "invalid_command",
|
|
284
|
+
"duration": 0.8,
|
|
285
|
+
"return_code": -1,
|
|
286
|
+
"child_pids": [],
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
290
|
+
|
|
291
|
+
result = await terminal_tool.check_command_status("terminal_1", "cmd_1")
|
|
292
|
+
|
|
293
|
+
assert "❌ Command terminated after 0.8s" in result
|
|
294
|
+
assert "Command: invalid_command" in result
|
|
295
|
+
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
async def test_check_command_status_not_found(self, terminal_tool, mock_connection_manager):
|
|
298
|
+
# Mock the terminal manager
|
|
299
|
+
mock_terminal_manager = Mock()
|
|
300
|
+
mock_terminal_manager.get_command_status = Mock(return_value={"status": "not_found"})
|
|
301
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
302
|
+
|
|
303
|
+
result = await terminal_tool.check_command_status("terminal_1", "invalid_cmd")
|
|
304
|
+
|
|
305
|
+
assert "❌ Command ID invalid_cmd not found" in result
|
|
306
|
+
|
|
307
|
+
@pytest.mark.asyncio
|
|
308
|
+
async def test_check_command_status_monitor_timeout(self, terminal_tool, mock_connection_manager):
|
|
309
|
+
mock_terminal_manager = Mock()
|
|
310
|
+
mock_terminal_manager.get_command_status = Mock(
|
|
311
|
+
return_value={
|
|
312
|
+
"status": "monitor_timeout",
|
|
313
|
+
"command": "sleep 999",
|
|
314
|
+
"duration": 300.5,
|
|
315
|
+
"return_code": None,
|
|
316
|
+
"child_pids": [],
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
320
|
+
|
|
321
|
+
result = await terminal_tool.check_command_status("terminal_1", "cmd_1")
|
|
322
|
+
|
|
323
|
+
assert "Command monitoring stopped after 300.5s" in result
|
|
324
|
+
assert "command may still be running in terminal terminal_1" in result
|
|
325
|
+
assert "Command: sleep 999" in result
|
|
326
|
+
assert 'check_command_status("terminal_1", "cmd_1")' in result
|
|
327
|
+
|
|
328
|
+
@pytest.mark.asyncio
|
|
329
|
+
async def test_check_terminal_status_with_active_commands(self, terminal_tool, mock_connection_manager):
|
|
330
|
+
# Mock the terminal manager
|
|
331
|
+
mock_terminal_manager = AsyncMock()
|
|
332
|
+
mock_terminal_manager.get_terminal_status = AsyncMock(
|
|
333
|
+
return_value={
|
|
334
|
+
"running": True,
|
|
335
|
+
"ready_for_commands": False,
|
|
336
|
+
"active_commands": {
|
|
337
|
+
"terminal_1_1": {"command": "sleep 30", "duration": 15.3},
|
|
338
|
+
"terminal_1_2": {"command": "npm test", "duration": 45.7},
|
|
339
|
+
},
|
|
340
|
+
"last_command": "npm test",
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
344
|
+
|
|
345
|
+
result = await terminal_tool.check_terminal_status("terminal_1")
|
|
346
|
+
|
|
347
|
+
assert "# Terminal terminal_1 Status" in result
|
|
348
|
+
assert "**Running:** Yes" in result
|
|
349
|
+
assert "**Ready for new commands:** No" in result
|
|
350
|
+
assert "`terminal_1_1`: sleep 30 (running 15.3s)" in result
|
|
351
|
+
assert "`terminal_1_2`: npm test (running 45.7s)" in result
|
|
352
|
+
|
|
353
|
+
@pytest.mark.asyncio
|
|
354
|
+
async def test_check_terminal_status_no_active_commands(self, terminal_tool, mock_connection_manager):
|
|
355
|
+
# Mock the terminal manager
|
|
356
|
+
mock_terminal_manager = AsyncMock()
|
|
357
|
+
mock_terminal_manager.get_terminal_status = AsyncMock(
|
|
358
|
+
return_value={"running": True, "ready_for_commands": True, "active_commands": {}, "last_command": None}
|
|
359
|
+
)
|
|
360
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
361
|
+
|
|
362
|
+
result = await terminal_tool.check_terminal_status("terminal_1")
|
|
363
|
+
|
|
364
|
+
assert "**Ready for new commands:** Yes" in result
|
|
365
|
+
assert "**Active Commands:** None" in result
|
|
366
|
+
|
|
367
|
+
@pytest.mark.asyncio
|
|
368
|
+
async def test_wait_for_command_completion_success(self, terminal_tool, mock_connection_manager):
|
|
369
|
+
# Mock the terminal manager to return completed status
|
|
370
|
+
mock_terminal_manager = Mock()
|
|
371
|
+
mock_terminal_manager.get_command_status = Mock(
|
|
372
|
+
return_value={"status": "completed", "command": "echo test", "duration": 2.1, "return_code": 0}
|
|
373
|
+
)
|
|
374
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
375
|
+
|
|
376
|
+
result = await terminal_tool.wait_for_command_completion("terminal_1", "cmd_1", timeout=5)
|
|
377
|
+
|
|
378
|
+
assert "✅ Command completed in 2.1s with exit code 0" in result
|
|
379
|
+
|
|
380
|
+
def test_normalize_wait_timeout(self, terminal_tool):
|
|
381
|
+
with patch.object(TerminalTool, "DEFAULT_COMMAND_WAIT_TIMEOUT_SECONDS", 120), patch.object(
|
|
382
|
+
TerminalTool, "MAX_COMMAND_WAIT_TIMEOUT_SECONDS", 300
|
|
383
|
+
):
|
|
384
|
+
assert terminal_tool._normalize_command_wait_timeout(None) == 120
|
|
385
|
+
assert terminal_tool._normalize_command_wait_timeout(0) == 120
|
|
386
|
+
assert terminal_tool._normalize_command_wait_timeout(-10) == 120
|
|
387
|
+
assert terminal_tool._normalize_command_wait_timeout("not-a-number") == 120
|
|
388
|
+
assert terminal_tool._normalize_command_wait_timeout(30) == 30
|
|
389
|
+
assert terminal_tool._normalize_command_wait_timeout(999) == 300
|
|
390
|
+
|
|
391
|
+
@pytest.mark.asyncio
|
|
392
|
+
async def test_wait_for_command_completion_timeout(self, terminal_tool, mock_connection_manager):
|
|
393
|
+
# Mock the terminal manager to always return running status
|
|
394
|
+
mock_terminal_manager = Mock()
|
|
395
|
+
mock_terminal_manager.get_command_status = Mock(
|
|
396
|
+
return_value={"status": "running", "command": "sleep 100", "duration": 10.0}
|
|
397
|
+
)
|
|
398
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
399
|
+
|
|
400
|
+
with patch("kolega_code.agent.tool_backend.terminal_tool.time.time", side_effect=[0, 0, 0, 2]), patch(
|
|
401
|
+
"kolega_code.agent.tool_backend.terminal_tool.asyncio.sleep", new_callable=AsyncMock
|
|
402
|
+
):
|
|
403
|
+
result = await terminal_tool.wait_for_command_completion("terminal_1", "cmd_1", timeout=1)
|
|
404
|
+
|
|
405
|
+
assert "⏰ Timeout: Command cmd_1 is still running in terminal terminal_1 after 1 seconds" in result
|
|
406
|
+
assert 'check_command_status("terminal_1", "cmd_1")' in result
|
|
407
|
+
|
|
408
|
+
@pytest.mark.asyncio
|
|
409
|
+
async def test_wait_for_command_completion_none_timeout_uses_default(self, terminal_tool, mock_connection_manager):
|
|
410
|
+
mock_terminal_manager = Mock()
|
|
411
|
+
mock_terminal_manager.get_command_status = Mock(
|
|
412
|
+
return_value={"status": "running", "command": "sleep 100", "duration": 10.0}
|
|
413
|
+
)
|
|
414
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
415
|
+
|
|
416
|
+
with patch.object(TerminalTool, "DEFAULT_COMMAND_WAIT_TIMEOUT_SECONDS", 1), patch(
|
|
417
|
+
"kolega_code.agent.tool_backend.terminal_tool.time.time", side_effect=[0, 0, 0, 2]
|
|
418
|
+
), patch("kolega_code.agent.tool_backend.terminal_tool.asyncio.sleep", new_callable=AsyncMock):
|
|
419
|
+
result = await terminal_tool.wait_for_command_completion("terminal_1", "cmd_1", timeout=None)
|
|
420
|
+
|
|
421
|
+
assert "after 1 seconds" in result
|
|
422
|
+
|
|
423
|
+
@pytest.mark.asyncio
|
|
424
|
+
async def test_wait_for_command_completion_timeout_is_clamped(self, terminal_tool, mock_connection_manager):
|
|
425
|
+
mock_terminal_manager = Mock()
|
|
426
|
+
mock_terminal_manager.get_command_status = Mock(
|
|
427
|
+
return_value={"status": "running", "command": "sleep 100", "duration": 10.0}
|
|
428
|
+
)
|
|
429
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
430
|
+
|
|
431
|
+
with patch.object(TerminalTool, "MAX_COMMAND_WAIT_TIMEOUT_SECONDS", 2), patch(
|
|
432
|
+
"kolega_code.agent.tool_backend.terminal_tool.time.time", side_effect=[0, 0, 0, 3]
|
|
433
|
+
), patch("kolega_code.agent.tool_backend.terminal_tool.asyncio.sleep", new_callable=AsyncMock):
|
|
434
|
+
result = await terminal_tool.wait_for_command_completion("terminal_1", "cmd_1", timeout=999)
|
|
435
|
+
|
|
436
|
+
assert "after 2 seconds" in result
|
|
437
|
+
|
|
438
|
+
@pytest.mark.asyncio
|
|
439
|
+
async def test_wait_for_command_completion_terminal_not_found(self, terminal_tool, mock_connection_manager):
|
|
440
|
+
# Mock the terminal manager to raise KeyError
|
|
441
|
+
mock_terminal_manager = Mock()
|
|
442
|
+
mock_terminal_manager.get_command_status = Mock(side_effect=KeyError("Terminal with ID invalid not found"))
|
|
443
|
+
terminal_tool.terminal_manager = mock_terminal_manager
|
|
444
|
+
|
|
445
|
+
result = await terminal_tool.wait_for_command_completion("invalid", "cmd_1", timeout=5)
|
|
446
|
+
|
|
447
|
+
assert "Terminal with ID invalid not found" in result
|
|
448
|
+
|
|
449
|
+
@pytest.mark.asyncio
|
|
450
|
+
async def test_read_terminal_with_offset_skips_compression(self, terminal_tool, mock_connection_manager):
|
|
451
|
+
"""Test that read_terminal with offset > 0 skips compression even for large output."""
|
|
452
|
+
# Create a large output that would normally trigger compression
|
|
453
|
+
large_output = "A" * 5000 # Exceeds the 4000 character threshold
|
|
454
|
+
terminal_tool.terminal_manager.read_output = Mock(return_value=large_output)
|
|
455
|
+
terminal_tool.terminal_manager.get_last_command = AsyncMock(return_value="test command")
|
|
456
|
+
terminal_tool.terminal_manager.get_last_command_purpose = AsyncMock(return_value="test purpose")
|
|
457
|
+
|
|
458
|
+
# Test with offset = 0 (should compress)
|
|
459
|
+
result_no_offset = await terminal_tool.read_terminal("test_terminal", num_chars=5000, offset=0)
|
|
460
|
+
assert "OUTPUT COMPRESSED" in result_no_offset
|
|
461
|
+
assert "offset parameter" in result_no_offset
|
|
462
|
+
|
|
463
|
+
# Test with offset > 0 (should not compress)
|
|
464
|
+
result_with_offset = await terminal_tool.read_terminal("test_terminal", num_chars=1000, offset=100)
|
|
465
|
+
assert "OUTPUT COMPRESSED" not in result_with_offset
|
|
466
|
+
assert result_with_offset.startswith("```\n")
|
|
467
|
+
assert result_with_offset.endswith("```\n")
|
|
468
|
+
|
|
469
|
+
@pytest.mark.asyncio
|
|
470
|
+
async def test_read_terminal_offset_parameter_passed_through(self, terminal_tool, mock_connection_manager):
|
|
471
|
+
"""Test that the offset parameter is correctly passed through to the terminal manager."""
|
|
472
|
+
terminal_tool.terminal_manager.read_output = Mock(return_value="test output")
|
|
473
|
+
|
|
474
|
+
# Test with various offset values
|
|
475
|
+
await terminal_tool.read_terminal("test_terminal", num_chars=500, offset=0)
|
|
476
|
+
terminal_tool.terminal_manager.read_output.assert_called_with("test_terminal", num_chars=500, offset=0)
|
|
477
|
+
|
|
478
|
+
await terminal_tool.read_terminal("test_terminal", num_chars=200, offset=50)
|
|
479
|
+
terminal_tool.terminal_manager.read_output.assert_called_with("test_terminal", num_chars=200, offset=50)
|
|
480
|
+
|
|
481
|
+
await terminal_tool.read_terminal("test_terminal", num_chars=1000, offset=100)
|
|
482
|
+
terminal_tool.terminal_manager.read_output.assert_called_with("test_terminal", num_chars=1000, offset=100)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Integration tests for the think_hard tool."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from kolega_code.config import AgentConfig, ModelConfig, ModelProvider, RateLimitConfig
|
|
8
|
+
from kolega_code.events import AgentConnectionManager
|
|
9
|
+
from kolega_code.agent.tool_backend.think_hard_tool import ThinkHardTool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_config():
|
|
14
|
+
"""Create a mock agent configuration."""
|
|
15
|
+
return AgentConfig(
|
|
16
|
+
anthropic_api_key="test-key",
|
|
17
|
+
openai_api_key="test-openai-key", # Required for edit_model_config
|
|
18
|
+
thinking_config=ModelConfig(
|
|
19
|
+
provider=ModelProvider.ANTHROPIC,
|
|
20
|
+
model="claude-3-7-sonnet-20250131",
|
|
21
|
+
rate_limits=RateLimitConfig(requests_per_minute=10, tokens_per_minute=100000, max_retries=3),
|
|
22
|
+
thinking_tokens=5000,
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def mock_connection_manager():
|
|
29
|
+
"""Create a mock connection manager."""
|
|
30
|
+
return AsyncMock(spec=AgentConnectionManager)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def mock_caller():
|
|
35
|
+
"""Create a mock caller (base agent)."""
|
|
36
|
+
mock = Mock()
|
|
37
|
+
mock.agent_name = "test_agent"
|
|
38
|
+
mock.user_id = "user-123"
|
|
39
|
+
mock.user_email = "user@example.com"
|
|
40
|
+
return mock
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def think_hard_tool(mock_config, mock_connection_manager, mock_caller):
|
|
45
|
+
"""Create a ThinkHardTool instance with mocked dependencies."""
|
|
46
|
+
tool = ThinkHardTool(
|
|
47
|
+
project_path="/test/path",
|
|
48
|
+
workspace_id="test_workspace",
|
|
49
|
+
thread_id="test_thread",
|
|
50
|
+
connection_manager=mock_connection_manager,
|
|
51
|
+
config=mock_config,
|
|
52
|
+
caller=mock_caller,
|
|
53
|
+
)
|
|
54
|
+
return tool
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_think_hard_tool_initialization(think_hard_tool):
|
|
59
|
+
"""Test that the think_hard tool initializes correctly."""
|
|
60
|
+
assert think_hard_tool.project_path == Path("/test/path")
|
|
61
|
+
assert think_hard_tool.workspace_id == "test_workspace"
|
|
62
|
+
assert think_hard_tool.thread_id == "test_thread"
|
|
63
|
+
assert think_hard_tool.config.thinking_config.thinking_tokens == 5000
|
|
64
|
+
assert think_hard_tool.config.thinking_config.provider == ModelProvider.ANTHROPIC
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_think_hard_method_exists(think_hard_tool):
|
|
69
|
+
"""Test that the think_hard method exists and is callable."""
|
|
70
|
+
assert hasattr(think_hard_tool, "think_hard")
|
|
71
|
+
assert callable(think_hard_tool.think_hard)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_think_hard_returns_string(think_hard_tool):
|
|
76
|
+
"""Test that think_hard returns a string response."""
|
|
77
|
+
# Mock the logging methods
|
|
78
|
+
think_hard_tool.log_info = AsyncMock()
|
|
79
|
+
think_hard_tool.log_error = AsyncMock()
|
|
80
|
+
|
|
81
|
+
# This would require actual API key to test fully, so we'll mock the LLMClient
|
|
82
|
+
with patch("kolega_code.agent.tool_backend.think_hard_tool.LLMClient") as mock_llm_class:
|
|
83
|
+
with patch("kolega_code.agent.tool_backend.think_hard_tool.get_model_specs") as mock_get_specs:
|
|
84
|
+
# Mock model specs
|
|
85
|
+
mock_get_specs.return_value = {"max_completion_tokens": 8192}
|
|
86
|
+
|
|
87
|
+
mock_llm_instance = mock_llm_class.return_value
|
|
88
|
+
|
|
89
|
+
# Create a simple mock stream that returns without actual API call
|
|
90
|
+
class SimpleStreamMock:
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.chunks = [] # No chunks to iterate over
|
|
93
|
+
self.chunk_index = 0
|
|
94
|
+
|
|
95
|
+
async def __aenter__(self):
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def __aiter__(self):
|
|
102
|
+
"""Make the stream async iterable."""
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
async def __anext__(self):
|
|
106
|
+
"""Return chunks for async iteration."""
|
|
107
|
+
if self.chunk_index >= len(self.chunks):
|
|
108
|
+
raise StopAsyncIteration
|
|
109
|
+
chunk = self.chunks[self.chunk_index]
|
|
110
|
+
self.chunk_index += 1
|
|
111
|
+
return chunk
|
|
112
|
+
|
|
113
|
+
async def get_final_message(self):
|
|
114
|
+
from kolega_code.llm.models import Message, TextBlock
|
|
115
|
+
|
|
116
|
+
return Message(role="assistant", content=[TextBlock(text="Test response")])
|
|
117
|
+
|
|
118
|
+
# stream method returns a coroutine that returns the mock stream
|
|
119
|
+
async def stream_coroutine(*args, **kwargs):
|
|
120
|
+
return SimpleStreamMock()
|
|
121
|
+
|
|
122
|
+
mock_llm_instance.stream = stream_coroutine
|
|
123
|
+
|
|
124
|
+
# Call think_hard
|
|
125
|
+
result = await think_hard_tool.think_hard("Test problem")
|
|
126
|
+
|
|
127
|
+
# Verify the result is a string
|
|
128
|
+
assert isinstance(result, str)
|
|
129
|
+
assert "# Final Analysis" in result
|
|
130
|
+
assert "Test response" in result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_think_hard_logging(think_hard_tool):
|
|
135
|
+
"""Test that think_hard logs appropriate messages."""
|
|
136
|
+
# Mock the logging methods
|
|
137
|
+
think_hard_tool.log_info = AsyncMock()
|
|
138
|
+
think_hard_tool.log_error = AsyncMock()
|
|
139
|
+
|
|
140
|
+
with patch("kolega_code.agent.tool_backend.think_hard_tool.LLMClient") as mock_llm_class:
|
|
141
|
+
with patch("kolega_code.agent.tool_backend.think_hard_tool.get_model_specs") as mock_get_specs:
|
|
142
|
+
# Mock model specs
|
|
143
|
+
mock_get_specs.return_value = {"max_completion_tokens": 8192}
|
|
144
|
+
|
|
145
|
+
mock_llm_instance = mock_llm_class.return_value
|
|
146
|
+
|
|
147
|
+
# Create a simple mock stream
|
|
148
|
+
class SimpleStreamMock:
|
|
149
|
+
def __init__(self):
|
|
150
|
+
self.chunks = [] # No chunks to iterate over
|
|
151
|
+
self.chunk_index = 0
|
|
152
|
+
|
|
153
|
+
async def __aenter__(self):
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def __aiter__(self):
|
|
160
|
+
"""Make the stream async iterable."""
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
async def __anext__(self):
|
|
164
|
+
"""Return chunks for async iteration."""
|
|
165
|
+
if self.chunk_index >= len(self.chunks):
|
|
166
|
+
raise StopAsyncIteration
|
|
167
|
+
chunk = self.chunks[self.chunk_index]
|
|
168
|
+
self.chunk_index += 1
|
|
169
|
+
return chunk
|
|
170
|
+
|
|
171
|
+
async def get_final_message(self):
|
|
172
|
+
from kolega_code.llm.models import Message, TextBlock
|
|
173
|
+
|
|
174
|
+
return Message(role="assistant", content=[TextBlock(text="Response")])
|
|
175
|
+
|
|
176
|
+
# stream method returns a coroutine that returns the mock stream
|
|
177
|
+
async def stream_coroutine(*args, **kwargs):
|
|
178
|
+
return SimpleStreamMock()
|
|
179
|
+
|
|
180
|
+
mock_llm_instance.stream = stream_coroutine
|
|
181
|
+
|
|
182
|
+
problem = "This is a very long problem statement that should be truncated in the log message"
|
|
183
|
+
await think_hard_tool.think_hard(problem)
|
|
184
|
+
|
|
185
|
+
# Verify logging was called
|
|
186
|
+
think_hard_tool.log_info.assert_called_once()
|
|
187
|
+
log_call = think_hard_tool.log_info.call_args[0][0]
|
|
188
|
+
assert "Thinking hard about:" in log_call
|
|
189
|
+
assert problem[:100] in log_call
|