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,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the PromptProvider class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from kolega_code.agent.prompt_provider import (
|
|
10
|
+
AgentMode,
|
|
11
|
+
AgentType,
|
|
12
|
+
MissingPromptTemplateError,
|
|
13
|
+
PromptContext,
|
|
14
|
+
PromptExtension,
|
|
15
|
+
PromptProvider,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestPromptProvider:
|
|
20
|
+
"""Test suite for PromptProvider functionality."""
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def prompt_provider(self):
|
|
24
|
+
"""Create a PromptProvider instance."""
|
|
25
|
+
return PromptProvider()
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def prompt_context(self):
|
|
29
|
+
"""Create a test prompt context."""
|
|
30
|
+
return PromptContext(
|
|
31
|
+
system_name="Kolega Code",
|
|
32
|
+
project_path="/test/project",
|
|
33
|
+
is_git_repo=True,
|
|
34
|
+
platform="Darwin",
|
|
35
|
+
date_today="2024-01-15",
|
|
36
|
+
model_name="claude-3-5-sonnet",
|
|
37
|
+
available_ports="9001-9999",
|
|
38
|
+
kolega_md="Test project documentation",
|
|
39
|
+
workspace_id="test-workspace-123",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def test_coder_agent_cli_mode(self, prompt_provider, prompt_context):
|
|
43
|
+
"""Test coder agent with public CLI mode."""
|
|
44
|
+
prompt = prompt_provider.get_system_prompt(
|
|
45
|
+
agent_type=AgentType.CODER, mode=AgentMode.CLI, context=prompt_context
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert prompt is not None
|
|
49
|
+
assert "local developer CLI" in prompt
|
|
50
|
+
assert "Kolega Code" in prompt
|
|
51
|
+
assert "/test/project" in prompt
|
|
52
|
+
assert "Test project documentation" in prompt
|
|
53
|
+
assert len(prompt) > 0
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("mode", [AgentMode.CODE, AgentMode.VIBE, AgentMode.FIX])
|
|
56
|
+
def test_hosted_coder_modes_require_host_template(self, prompt_provider, prompt_context, mode):
|
|
57
|
+
"""Hosted coder modes are private and require host-owned prompt templates."""
|
|
58
|
+
with pytest.raises(MissingPromptTemplateError) as exc_info:
|
|
59
|
+
prompt_provider.get_system_prompt(agent_type=AgentType.CODER, mode=mode, context=prompt_context)
|
|
60
|
+
|
|
61
|
+
assert mode.value in str(exc_info.value)
|
|
62
|
+
assert "host-owned template_dirs" in str(exc_info.value)
|
|
63
|
+
|
|
64
|
+
def test_cli_mode_prompt_omits_platform_memory_bank_instructions(self, prompt_provider, prompt_context):
|
|
65
|
+
"""CLI mode should not include platform memory-bank or hosted-agent instructions."""
|
|
66
|
+
prompt = prompt_provider.get_system_prompt(
|
|
67
|
+
agent_type=AgentType.CODER, mode=AgentMode.CLI, context=prompt_context
|
|
68
|
+
)
|
|
69
|
+
prompt_lower = prompt.lower()
|
|
70
|
+
|
|
71
|
+
assert "memory bank" not in prompt_lower
|
|
72
|
+
assert "kolega-memory-bank" not in prompt
|
|
73
|
+
assert "dispatch_investigation_agent" not in prompt
|
|
74
|
+
assert "Test Task Detection" not in prompt
|
|
75
|
+
assert "Scope Boundary Management" not in prompt
|
|
76
|
+
|
|
77
|
+
def test_investigation_agent_prompt_generation(self, prompt_provider, prompt_context):
|
|
78
|
+
"""Test that investigation agent prompts can be generated."""
|
|
79
|
+
prompt = prompt_provider.get_system_prompt(agent_type=AgentType.INVESTIGATION, context=prompt_context)
|
|
80
|
+
|
|
81
|
+
assert prompt is not None
|
|
82
|
+
assert len(prompt) > 0
|
|
83
|
+
assert "code investigation agent" in prompt
|
|
84
|
+
assert "explaining a codebase" in prompt
|
|
85
|
+
assert "/test/project" in prompt
|
|
86
|
+
|
|
87
|
+
def test_browser_agent_prompt_generation(self, prompt_provider, prompt_context):
|
|
88
|
+
"""Test that browser agent prompts can be generated."""
|
|
89
|
+
prompt = prompt_provider.get_system_prompt(agent_type=AgentType.BROWSER, context=prompt_context)
|
|
90
|
+
|
|
91
|
+
assert prompt is not None
|
|
92
|
+
assert len(prompt) > 0
|
|
93
|
+
assert "web browser agent" in prompt
|
|
94
|
+
assert "QA on a web application" in prompt
|
|
95
|
+
assert "URL Navigation Guidelines" in prompt
|
|
96
|
+
|
|
97
|
+
def test_cli_prompt_with_matching_prompt_extension(self, prompt_provider, prompt_context):
|
|
98
|
+
"""CLI mode should render prompt extensions that target CLI mode."""
|
|
99
|
+
prompt = prompt_provider.get_system_prompt(
|
|
100
|
+
agent_type=AgentType.CODER,
|
|
101
|
+
mode=AgentMode.CLI,
|
|
102
|
+
prompt_extensions=[
|
|
103
|
+
PromptExtension(
|
|
104
|
+
id="cli-example",
|
|
105
|
+
title="CLI Extension",
|
|
106
|
+
markdown="Extra CLI context.",
|
|
107
|
+
agent_types=[AgentType.CODER],
|
|
108
|
+
modes=[AgentMode.CLI],
|
|
109
|
+
)
|
|
110
|
+
],
|
|
111
|
+
context=prompt_context,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
assert prompt is not None
|
|
115
|
+
assert "CLI Extension" in prompt
|
|
116
|
+
assert "Extra CLI context." in prompt
|
|
117
|
+
|
|
118
|
+
def test_prompt_filters_non_matching_prompt_extension(self, prompt_provider, prompt_context):
|
|
119
|
+
"""Prompt extensions should only render for matching agent types and modes."""
|
|
120
|
+
prompt = prompt_provider.get_system_prompt(
|
|
121
|
+
agent_type=AgentType.CODER,
|
|
122
|
+
mode=AgentMode.CLI,
|
|
123
|
+
prompt_extensions=[
|
|
124
|
+
PromptExtension(
|
|
125
|
+
id="browser-only",
|
|
126
|
+
title="Browser Only",
|
|
127
|
+
markdown="This should not render.",
|
|
128
|
+
agent_types=[AgentType.BROWSER],
|
|
129
|
+
)
|
|
130
|
+
],
|
|
131
|
+
context=prompt_context,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert prompt is not None
|
|
135
|
+
assert "Browser Only" not in prompt
|
|
136
|
+
assert "This should not render." not in prompt
|
|
137
|
+
|
|
138
|
+
def test_cli_mode_prompt_includes_workspace_environment_variables(self, prompt_provider, prompt_context):
|
|
139
|
+
"""Coder CLI mode should list workspace environment variable descriptions when provided."""
|
|
140
|
+
prompt_context.workspace_environment_variables = {
|
|
141
|
+
"STRIPE_API_KEY": "Stripe API key for billing",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
prompt = prompt_provider.get_system_prompt(
|
|
145
|
+
agent_type=AgentType.CODER, mode=AgentMode.CLI, context=prompt_context
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
assert "STRIPE_API_KEY" in prompt
|
|
149
|
+
assert "Stripe API key for billing" in prompt
|
|
150
|
+
|
|
151
|
+
def test_host_template_dir_supplies_hosted_mode_prompt(self, tmp_path, prompt_context):
|
|
152
|
+
"""Host template dirs can provide private hosted-mode prompts."""
|
|
153
|
+
agents_dir = tmp_path / "agents"
|
|
154
|
+
agents_dir.mkdir()
|
|
155
|
+
(agents_dir / "coder_code_mode.j2").write_text(
|
|
156
|
+
"Private {{ context.system_name }} prompt for {{ mode }} at {{ context.project_path }}",
|
|
157
|
+
encoding="utf-8",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
prompt = PromptProvider(template_dirs=[tmp_path]).get_system_prompt(
|
|
161
|
+
agent_type=AgentType.CODER,
|
|
162
|
+
mode=AgentMode.CODE,
|
|
163
|
+
context=prompt_context,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
assert prompt == "Private Kolega Code prompt for code at /test/project"
|
|
167
|
+
|
|
168
|
+
def test_host_template_dir_can_use_builtin_includes(self, tmp_path, prompt_context):
|
|
169
|
+
"""Private templates can still include bundled generic snippets."""
|
|
170
|
+
agents_dir = tmp_path / "agents"
|
|
171
|
+
agents_dir.mkdir()
|
|
172
|
+
(agents_dir / "coder_vibe_mode.j2").write_text(
|
|
173
|
+
"{% include 'environment_variables/workspace_env_vars.md' %}",
|
|
174
|
+
encoding="utf-8",
|
|
175
|
+
)
|
|
176
|
+
prompt_context.workspace_environment_variables = {"PAYMENTS_REGION": "Region for payment processor"}
|
|
177
|
+
|
|
178
|
+
prompt = PromptProvider(template_dirs=[tmp_path]).get_system_prompt(
|
|
179
|
+
agent_type=AgentType.CODER,
|
|
180
|
+
mode=AgentMode.VIBE,
|
|
181
|
+
context=prompt_context,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
assert "PAYMENTS_REGION" in prompt
|
|
185
|
+
assert "Region for payment processor" in prompt
|
|
186
|
+
|
|
187
|
+
def test_hosted_prompt_with_matching_prompt_extension(self, tmp_path, prompt_context):
|
|
188
|
+
"""Private hosted prompts still receive filtered prompt extensions."""
|
|
189
|
+
agents_dir = tmp_path / "agents"
|
|
190
|
+
agents_dir.mkdir()
|
|
191
|
+
(agents_dir / "coder_fix_mode.j2").write_text(
|
|
192
|
+
"{% for extension in prompt_extensions %}{{ extension.title }}: {{ extension.markdown }}{% endfor %}",
|
|
193
|
+
encoding="utf-8",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
prompt = PromptProvider(template_dirs=[tmp_path]).get_system_prompt(
|
|
197
|
+
agent_type=AgentType.CODER,
|
|
198
|
+
mode=AgentMode.FIX,
|
|
199
|
+
prompt_extensions=[
|
|
200
|
+
PromptExtension(
|
|
201
|
+
id="fix-example",
|
|
202
|
+
title="Fix Extension",
|
|
203
|
+
markdown="Extra fix context.",
|
|
204
|
+
agent_types=[AgentType.CODER],
|
|
205
|
+
modes=[AgentMode.FIX],
|
|
206
|
+
)
|
|
207
|
+
],
|
|
208
|
+
context=prompt_context,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
assert "Fix Extension: Extra fix context." in prompt
|
|
212
|
+
|
|
213
|
+
def test_prompt_with_template_slug_in_cli_mode(self, prompt_provider, prompt_context):
|
|
214
|
+
"""Template guidance is still available to public CLI mode."""
|
|
215
|
+
prompt = prompt_provider.get_system_prompt(
|
|
216
|
+
agent_type=AgentType.CODER,
|
|
217
|
+
mode=AgentMode.CLI,
|
|
218
|
+
template_slug="mern-stack-template",
|
|
219
|
+
context=prompt_context,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
assert prompt is not None
|
|
223
|
+
assert "Project Starter Template" in prompt
|
|
224
|
+
|
|
225
|
+
def test_minimal_context(self, prompt_provider):
|
|
226
|
+
"""Test prompt generation with minimal context."""
|
|
227
|
+
prompt = prompt_provider.get_system_prompt(agent_type=AgentType.CODER, mode=AgentMode.CLI)
|
|
228
|
+
|
|
229
|
+
assert prompt is not None
|
|
230
|
+
assert "Kolega Code" in prompt
|
|
231
|
+
|
|
232
|
+
def test_templates_can_be_loaded(self, prompt_provider):
|
|
233
|
+
"""Test that public templates load successfully from the template directory."""
|
|
234
|
+
prompt = prompt_provider.get_system_prompt(agent_type=AgentType.CODER, mode=AgentMode.CLI)
|
|
235
|
+
assert prompt is not None
|
|
236
|
+
assert len(prompt) > 0
|
|
237
|
+
|
|
238
|
+
def test_all_public_agent_types(self, prompt_provider, prompt_context):
|
|
239
|
+
"""Test that all public agent types can generate prompts."""
|
|
240
|
+
for agent_type in AgentType:
|
|
241
|
+
mode = AgentMode.CLI if agent_type == AgentType.CODER else None
|
|
242
|
+
prompt = prompt_provider.get_system_prompt(agent_type=agent_type, mode=mode, context=prompt_context)
|
|
243
|
+
|
|
244
|
+
assert prompt is not None
|
|
245
|
+
assert len(prompt) > 0
|
|
246
|
+
if agent_type == AgentType.CODER:
|
|
247
|
+
assert "powerful AI coding assistant" in prompt
|
|
248
|
+
elif agent_type == AgentType.INVESTIGATION:
|
|
249
|
+
assert "code investigation agent" in prompt
|
|
250
|
+
elif agent_type == AgentType.BROWSER:
|
|
251
|
+
assert "web browser agent" in prompt
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_public_package_does_not_contain_private_hosted_prompt_markers():
|
|
255
|
+
package_root = Path(__file__).parents[2]
|
|
256
|
+
forbidden_markers = [
|
|
257
|
+
"app.kolega.studio/kolega-error-reporter.js",
|
|
258
|
+
"kolega-memory-bank",
|
|
259
|
+
"Test Task Detection",
|
|
260
|
+
"Scope Boundary Management",
|
|
261
|
+
"You are operating in **fix mode**",
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
for path in package_root.rglob("*"):
|
|
265
|
+
if not path.is_file() or path.suffix not in {".py", ".j2", ".md"}:
|
|
266
|
+
continue
|
|
267
|
+
if "__pycache__" in path.parts or path.name == "test_prompt_provider.py":
|
|
268
|
+
continue
|
|
269
|
+
text = path.read_text(encoding="utf-8")
|
|
270
|
+
for marker in forbidden_markers:
|
|
271
|
+
assert marker not in text, f"{marker!r} leaked into {path}"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for the first-class tool primitives (Tool, ToolRegistry, ToolPolicy)."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from kolega_code.llm.models import ToolDefinition
|
|
6
|
+
from kolega_code.tools import Tool, ToolPolicy, ToolRegistry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def make_tool(name: str, *, parallel_safe: bool = False, result: str = "ok") -> Tool:
|
|
10
|
+
async def handler(**inputs):
|
|
11
|
+
return f"{result}:{inputs.get('arg', '')}"
|
|
12
|
+
|
|
13
|
+
return Tool(
|
|
14
|
+
name=name,
|
|
15
|
+
definition=ToolDefinition(name=name, description=f"{name} tool", parameters=[]),
|
|
16
|
+
handler=handler,
|
|
17
|
+
parallel_safe=parallel_safe,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestToolRegistry:
|
|
22
|
+
def test_add_and_lookup(self):
|
|
23
|
+
registry = ToolRegistry().add(make_tool("read"), make_tool("write"))
|
|
24
|
+
assert "read" in registry
|
|
25
|
+
assert "missing" not in registry
|
|
26
|
+
assert registry.get("write").name == "write"
|
|
27
|
+
assert registry.names() == ["read", "write"]
|
|
28
|
+
|
|
29
|
+
def test_duplicate_registration_rejected(self):
|
|
30
|
+
registry = ToolRegistry().add(make_tool("read"))
|
|
31
|
+
with pytest.raises(ValueError, match="already registered"):
|
|
32
|
+
registry.add(make_tool("read"))
|
|
33
|
+
|
|
34
|
+
@pytest.mark.asyncio
|
|
35
|
+
async def test_call_dispatches_by_name(self):
|
|
36
|
+
registry = ToolRegistry().add(make_tool("read", result="contents"))
|
|
37
|
+
assert await registry.call("read", arg="x") == "contents:x"
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_call_unknown_tool_raises(self):
|
|
41
|
+
with pytest.raises(KeyError):
|
|
42
|
+
await ToolRegistry().call("nope")
|
|
43
|
+
|
|
44
|
+
def test_select_applies_policy(self):
|
|
45
|
+
registry = ToolRegistry().add(make_tool("read"), make_tool("write"), make_tool("delete"))
|
|
46
|
+
|
|
47
|
+
excluded = registry.select(ToolPolicy(exclude=frozenset({"delete"})))
|
|
48
|
+
assert excluded.names() == ["read", "write"]
|
|
49
|
+
|
|
50
|
+
allowlisted = registry.select(ToolPolicy(include=frozenset({"write"})))
|
|
51
|
+
assert allowlisted.names() == ["write"]
|
|
52
|
+
|
|
53
|
+
def test_definitions_put_cache_checkpoint_on_last_only(self):
|
|
54
|
+
registry = ToolRegistry().add(make_tool("a"), make_tool("b"), make_tool("c"))
|
|
55
|
+
|
|
56
|
+
definitions = registry.definitions()
|
|
57
|
+
assert [d.cache_checkpoint for d in definitions] == [False, False, True]
|
|
58
|
+
|
|
59
|
+
# A subset view moves the checkpoint to its own last definition
|
|
60
|
+
subset = registry.select(ToolPolicy(exclude=frozenset({"c"})))
|
|
61
|
+
subset_definitions = subset.definitions()
|
|
62
|
+
assert [d.name for d in subset_definitions] == ["a", "b"]
|
|
63
|
+
assert [d.cache_checkpoint for d in subset_definitions] == [False, True]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestToolCollectionRegistry:
|
|
67
|
+
def test_registry_marks_parallel_safety_from_groups(self, tmp_path):
|
|
68
|
+
from unittest.mock import Mock
|
|
69
|
+
|
|
70
|
+
from kolega_code.agent.tools import ToolCollection
|
|
71
|
+
|
|
72
|
+
collection = ToolCollection(
|
|
73
|
+
tmp_path,
|
|
74
|
+
"ws",
|
|
75
|
+
"thread",
|
|
76
|
+
Mock(),
|
|
77
|
+
Mock(),
|
|
78
|
+
Mock(agent_name="test"),
|
|
79
|
+
)
|
|
80
|
+
registry = collection.registry()
|
|
81
|
+
|
|
82
|
+
assert registry.get("read_entire_file").parallel_safe
|
|
83
|
+
assert registry.get("search_codebase").parallel_safe
|
|
84
|
+
assert not registry.get("create_file").parallel_safe
|
|
85
|
+
assert not registry.get("run_command_tracked").parallel_safe
|
|
86
|
+
|
|
87
|
+
def test_get_tool_list_matches_registry_definitions(self, tmp_path):
|
|
88
|
+
from unittest.mock import Mock
|
|
89
|
+
|
|
90
|
+
from kolega_code.agent.tools import ToolCollection
|
|
91
|
+
|
|
92
|
+
collection = ToolCollection(
|
|
93
|
+
tmp_path,
|
|
94
|
+
"ws",
|
|
95
|
+
"thread",
|
|
96
|
+
Mock(),
|
|
97
|
+
Mock(),
|
|
98
|
+
Mock(agent_name="test"),
|
|
99
|
+
)
|
|
100
|
+
names_from_list = [d.name for d in collection.get_tool_list()]
|
|
101
|
+
registry_names = [tool.name for tool in collection.registry()]
|
|
102
|
+
assert names_from_list == registry_names
|