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,171 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from kolega_code.config import ModelProvider
|
|
6
|
+
from kolega_code.cli.config import (
|
|
7
|
+
DEFAULT_EDIT_MODEL,
|
|
8
|
+
DEFAULT_FAST_MODEL,
|
|
9
|
+
DEFAULT_LONG_MODEL,
|
|
10
|
+
DEFAULT_THINKING_MODEL,
|
|
11
|
+
CliConfigError,
|
|
12
|
+
CliConfigOverrides,
|
|
13
|
+
build_agent_config,
|
|
14
|
+
config_summary,
|
|
15
|
+
)
|
|
16
|
+
from kolega_code.cli.provider_registry import DEEPSEEK_DEFAULT_MODEL, UI_DEFAULT_MODEL, UI_DEFAULT_PROVIDER
|
|
17
|
+
from kolega_code.cli.settings import CliSettings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_build_agent_config_defaults_to_latest_anthropic_models(tmp_path: Path) -> None:
|
|
21
|
+
config = build_agent_config(tmp_path, env={"ANTHROPIC_API_KEY": "test-key"})
|
|
22
|
+
|
|
23
|
+
assert config.long_context_config.provider == ModelProvider.ANTHROPIC
|
|
24
|
+
assert config.long_context_config.model == DEFAULT_LONG_MODEL
|
|
25
|
+
assert config.fast_config.model == DEFAULT_FAST_MODEL
|
|
26
|
+
assert config.edit_model_config.model == DEFAULT_EDIT_MODEL
|
|
27
|
+
assert config.thinking_config.model == DEFAULT_THINKING_MODEL
|
|
28
|
+
assert config.thinking_config.thinking_tokens == 1024
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_build_agent_config_env_overrides(tmp_path: Path) -> None:
|
|
32
|
+
config = build_agent_config(
|
|
33
|
+
tmp_path,
|
|
34
|
+
env={
|
|
35
|
+
"ANTHROPIC_API_KEY": "test-key",
|
|
36
|
+
"KOLEGA_CODE_MODEL": "claude-sonnet-4-6",
|
|
37
|
+
"KOLEGA_CODE_THINKING_TOKENS": "2048",
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert config.long_context_config.model == "claude-sonnet-4-6"
|
|
42
|
+
assert config.thinking_config.thinking_tokens == 2048
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_build_agent_config_flags_override_env(tmp_path: Path) -> None:
|
|
46
|
+
config = build_agent_config(
|
|
47
|
+
tmp_path,
|
|
48
|
+
CliConfigOverrides(model="claude-opus-4-7", thinking_tokens=4096),
|
|
49
|
+
env={
|
|
50
|
+
"ANTHROPIC_API_KEY": "test-key",
|
|
51
|
+
"KOLEGA_CODE_MODEL": "claude-sonnet-4-6",
|
|
52
|
+
"KOLEGA_CODE_THINKING_TOKENS": "2048",
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert config.long_context_config.model == "claude-opus-4-7"
|
|
57
|
+
assert config.thinking_config.thinking_tokens == 4096
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_build_agent_config_requires_api_key(tmp_path: Path) -> None:
|
|
61
|
+
with pytest.raises(CliConfigError, match="ANTHROPIC_API_KEY"):
|
|
62
|
+
build_agent_config(tmp_path, env={})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_build_agent_config_rejects_unknown_model(tmp_path: Path) -> None:
|
|
66
|
+
with pytest.raises(CliConfigError, match="not supported"):
|
|
67
|
+
build_agent_config(tmp_path, CliConfigOverrides(model="claude-not-real"), env={"ANTHROPIC_API_KEY": "key"})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_config_summary_excludes_api_keys(tmp_path: Path) -> None:
|
|
71
|
+
config = build_agent_config(tmp_path, env={"ANTHROPIC_API_KEY": "secret-value"})
|
|
72
|
+
|
|
73
|
+
summary = config_summary(config)
|
|
74
|
+
|
|
75
|
+
assert summary["long_model"] == DEFAULT_LONG_MODEL
|
|
76
|
+
assert "secret-value" not in str(summary)
|
|
77
|
+
assert "api_key" not in str(summary).lower()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_build_agent_config_uses_stored_kimi_for_all_model_slots(tmp_path: Path) -> None:
|
|
81
|
+
settings = CliSettings(active_provider=UI_DEFAULT_PROVIDER, active_model=UI_DEFAULT_MODEL)
|
|
82
|
+
settings.set_api_key(UI_DEFAULT_PROVIDER, "moonshot-key")
|
|
83
|
+
|
|
84
|
+
config = build_agent_config(tmp_path, settings=settings, env={})
|
|
85
|
+
|
|
86
|
+
assert config.long_context_config.provider == ModelProvider.MOONSHOT
|
|
87
|
+
assert config.long_context_config.model == UI_DEFAULT_MODEL
|
|
88
|
+
assert config.fast_config.provider == ModelProvider.MOONSHOT
|
|
89
|
+
assert config.fast_config.model == UI_DEFAULT_MODEL
|
|
90
|
+
assert config.edit_model_config.provider == ModelProvider.MOONSHOT
|
|
91
|
+
assert config.edit_model_config.model == UI_DEFAULT_MODEL
|
|
92
|
+
assert config.thinking_config.provider == ModelProvider.MOONSHOT
|
|
93
|
+
assert config.thinking_config.model == UI_DEFAULT_MODEL
|
|
94
|
+
assert config.moonshot_api_key == "moonshot-key"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_build_agent_config_uses_stored_deepseek_for_all_model_slots(tmp_path: Path) -> None:
|
|
98
|
+
settings = CliSettings(active_provider=ModelProvider.DEEPSEEK.value, active_model=DEEPSEEK_DEFAULT_MODEL)
|
|
99
|
+
settings.set_api_key(ModelProvider.DEEPSEEK.value, "deepseek-key")
|
|
100
|
+
|
|
101
|
+
config = build_agent_config(tmp_path, settings=settings, env={})
|
|
102
|
+
|
|
103
|
+
assert config.long_context_config.provider == ModelProvider.DEEPSEEK
|
|
104
|
+
assert config.long_context_config.model == DEEPSEEK_DEFAULT_MODEL
|
|
105
|
+
assert config.fast_config.provider == ModelProvider.DEEPSEEK
|
|
106
|
+
assert config.fast_config.model == DEEPSEEK_DEFAULT_MODEL
|
|
107
|
+
assert config.edit_model_config.provider == ModelProvider.DEEPSEEK
|
|
108
|
+
assert config.edit_model_config.model == DEEPSEEK_DEFAULT_MODEL
|
|
109
|
+
assert config.thinking_config.provider == ModelProvider.DEEPSEEK
|
|
110
|
+
assert config.thinking_config.model == DEEPSEEK_DEFAULT_MODEL
|
|
111
|
+
assert config.deepseek_api_key == "deepseek-key"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_build_agent_config_accepts_moonshot_cli_active_model(tmp_path: Path) -> None:
|
|
115
|
+
config = build_agent_config(
|
|
116
|
+
tmp_path,
|
|
117
|
+
CliConfigOverrides(provider=UI_DEFAULT_PROVIDER, model=UI_DEFAULT_MODEL),
|
|
118
|
+
env={"MOONSHOT_API_KEY": "moonshot-key"},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert config.long_context_config.provider == ModelProvider.MOONSHOT
|
|
122
|
+
assert config.fast_config.provider == ModelProvider.MOONSHOT
|
|
123
|
+
assert config.edit_model_config.provider == ModelProvider.MOONSHOT
|
|
124
|
+
assert config.thinking_config.provider == ModelProvider.MOONSHOT
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_build_agent_config_accepts_deepseek_cli_active_model(tmp_path: Path) -> None:
|
|
128
|
+
config = build_agent_config(
|
|
129
|
+
tmp_path,
|
|
130
|
+
CliConfigOverrides(provider=ModelProvider.DEEPSEEK.value, model=DEEPSEEK_DEFAULT_MODEL),
|
|
131
|
+
env={"DEEPSEEK_API_KEY": "deepseek-key"},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert config.long_context_config.provider == ModelProvider.DEEPSEEK
|
|
135
|
+
assert config.fast_config.provider == ModelProvider.DEEPSEEK
|
|
136
|
+
assert config.edit_model_config.provider == ModelProvider.DEEPSEEK
|
|
137
|
+
assert config.thinking_config.provider == ModelProvider.DEEPSEEK
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_env_provider_model_overrides_stored_settings(tmp_path: Path) -> None:
|
|
141
|
+
settings = CliSettings(active_provider=UI_DEFAULT_PROVIDER, active_model=UI_DEFAULT_MODEL)
|
|
142
|
+
settings.set_api_key(UI_DEFAULT_PROVIDER, "moonshot-key")
|
|
143
|
+
|
|
144
|
+
config = build_agent_config(
|
|
145
|
+
tmp_path,
|
|
146
|
+
settings=settings,
|
|
147
|
+
env={
|
|
148
|
+
"ANTHROPIC_API_KEY": "anthropic-key",
|
|
149
|
+
"KOLEGA_CODE_PROVIDER": "anthropic",
|
|
150
|
+
"KOLEGA_CODE_MODEL": "claude-sonnet-4-6",
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert config.long_context_config.provider == ModelProvider.ANTHROPIC
|
|
155
|
+
assert config.long_context_config.model == "claude-sonnet-4-6"
|
|
156
|
+
assert config.fast_config.provider == ModelProvider.ANTHROPIC
|
|
157
|
+
assert config.fast_config.model == "claude-sonnet-4-6"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_stored_kimi_settings_require_moonshot_key(tmp_path: Path) -> None:
|
|
161
|
+
settings = CliSettings(active_provider=UI_DEFAULT_PROVIDER, active_model=UI_DEFAULT_MODEL)
|
|
162
|
+
|
|
163
|
+
with pytest.raises(CliConfigError, match="MOONSHOT_API_KEY"):
|
|
164
|
+
build_agent_config(tmp_path, settings=settings, env={})
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_stored_deepseek_settings_require_deepseek_key(tmp_path: Path) -> None:
|
|
168
|
+
settings = CliSettings(active_provider=ModelProvider.DEEPSEEK.value, active_model=DEEPSEEK_DEFAULT_MODEL)
|
|
169
|
+
|
|
170
|
+
with pytest.raises(CliConfigError, match="DEEPSEEK_API_KEY"):
|
|
171
|
+
build_agent_config(tmp_path, settings=settings, env={})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from kolega_code.events import AgentEvent
|
|
4
|
+
from kolega_code.cli.connection import CliConnectionManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_cli_connection_manager_queues_broadcast_events() -> None:
|
|
9
|
+
manager = CliConnectionManager()
|
|
10
|
+
event = AgentEvent(event_type="log_message", sender="test", content={"text": "hello"})
|
|
11
|
+
|
|
12
|
+
await manager.broadcast_event(event, "workspace", "thread")
|
|
13
|
+
|
|
14
|
+
assert await manager.next_event() == event
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.asyncio
|
|
18
|
+
async def test_cli_connection_manager_counts_connections() -> None:
|
|
19
|
+
manager = CliConnectionManager()
|
|
20
|
+
|
|
21
|
+
await manager.connect(object(), "workspace", "thread", "chat")
|
|
22
|
+
await manager.connect(object(), "workspace", "thread", "chat")
|
|
23
|
+
await manager.connect(object(), "workspace", "thread", "logs")
|
|
24
|
+
manager.disconnect(object(), "workspace", "thread", "chat")
|
|
25
|
+
|
|
26
|
+
assert manager.get_connection_count("workspace", "thread") == {"chat": 1, "logs": 1}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Tests for the workspace file index behind @ mention autocomplete."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from kolega_code.cli.file_index import WorkspaceFileIndex, fuzzy_score
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _paths(entries) -> list[str]:
|
|
9
|
+
return [entry.path for entry in entries]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_index_lists_files_and_directories(tmp_path: Path) -> None:
|
|
13
|
+
(tmp_path / "src").mkdir()
|
|
14
|
+
(tmp_path / "src" / "main.py").write_text("", encoding="utf-8")
|
|
15
|
+
(tmp_path / "README.md").write_text("", encoding="utf-8")
|
|
16
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
17
|
+
paths = _paths(index.entries())
|
|
18
|
+
assert "src/" in paths
|
|
19
|
+
assert "src/main.py" in paths
|
|
20
|
+
assert "README.md" in paths
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_index_prunes_excluded_directories(tmp_path: Path) -> None:
|
|
24
|
+
(tmp_path / "node_modules" / "pkg").mkdir(parents=True)
|
|
25
|
+
(tmp_path / "node_modules" / "pkg" / "index.js").write_text("", encoding="utf-8")
|
|
26
|
+
(tmp_path / "app.js").write_text("", encoding="utf-8")
|
|
27
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
28
|
+
paths = _paths(index.entries())
|
|
29
|
+
assert paths == ["app.js"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_index_respects_gitignore(tmp_path: Path) -> None:
|
|
33
|
+
(tmp_path / ".gitignore").write_text("ignored.txt\nsecrets/\n", encoding="utf-8")
|
|
34
|
+
(tmp_path / "ignored.txt").write_text("", encoding="utf-8")
|
|
35
|
+
(tmp_path / "kept.txt").write_text("", encoding="utf-8")
|
|
36
|
+
(tmp_path / "secrets").mkdir()
|
|
37
|
+
(tmp_path / "secrets" / "key.pem").write_text("", encoding="utf-8")
|
|
38
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
39
|
+
paths = _paths(index.entries())
|
|
40
|
+
assert "kept.txt" in paths
|
|
41
|
+
assert "ignored.txt" not in paths
|
|
42
|
+
assert all(not path.startswith("secrets") for path in paths)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_index_skips_binary_extensions(tmp_path: Path) -> None:
|
|
46
|
+
(tmp_path / "image.png").write_bytes(b"png")
|
|
47
|
+
(tmp_path / "code.py").write_text("", encoding="utf-8")
|
|
48
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
49
|
+
paths = _paths(index.entries())
|
|
50
|
+
assert "code.py" in paths
|
|
51
|
+
assert "image.png" not in paths
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_index_caps_total_entries(tmp_path: Path, monkeypatch) -> None:
|
|
55
|
+
monkeypatch.setattr(WorkspaceFileIndex, "MAX_FILES", 10)
|
|
56
|
+
for index_num in range(25):
|
|
57
|
+
(tmp_path / f"f{index_num:03}.txt").write_text("", encoding="utf-8")
|
|
58
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
59
|
+
assert len(index.entries()) == 10
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_index_refreshes_after_ttl(tmp_path: Path, monkeypatch) -> None:
|
|
63
|
+
monkeypatch.setattr(WorkspaceFileIndex, "TTL_SECONDS", 0.0)
|
|
64
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
65
|
+
assert index.entries() == []
|
|
66
|
+
(tmp_path / "late.txt").write_text("", encoding="utf-8")
|
|
67
|
+
assert _paths(index.entries()) == ["late.txt"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_search_ranks_substring_above_subsequence(tmp_path: Path) -> None:
|
|
71
|
+
(tmp_path / "main.py").write_text("", encoding="utf-8")
|
|
72
|
+
(tmp_path / "mandatory_ai_notes.txt").write_text("", encoding="utf-8")
|
|
73
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
74
|
+
results = _paths(index.search("main"))
|
|
75
|
+
assert results[0] == "main.py"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_search_empty_query_prefers_shallow_paths(tmp_path: Path) -> None:
|
|
79
|
+
(tmp_path / "deep" / "nest").mkdir(parents=True)
|
|
80
|
+
(tmp_path / "deep" / "nest" / "leaf.txt").write_text("", encoding="utf-8")
|
|
81
|
+
(tmp_path / "top.txt").write_text("", encoding="utf-8")
|
|
82
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
83
|
+
results = _paths(index.search(""))
|
|
84
|
+
assert results[0] == "top.txt"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_fuzzy_score_no_match_returns_none() -> None:
|
|
88
|
+
assert fuzzy_score("zzz", "src/main.py") is None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_fuzzy_score_subsequence_matches() -> None:
|
|
92
|
+
assert fuzzy_score("smain", "src/main.py") is not None
|
|
93
|
+
assert fuzzy_score("apppy", "kolega_code/cli/app.py") is not None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_search_dense_basename_match_beats_scattered_path_match(tmp_path: Path) -> None:
|
|
97
|
+
(tmp_path / "cli").mkdir()
|
|
98
|
+
(tmp_path / "agent").mkdir()
|
|
99
|
+
(tmp_path / "cli" / "app.py").write_text("", encoding="utf-8")
|
|
100
|
+
(tmp_path / "agent" / "prompts.py").write_text("", encoding="utf-8")
|
|
101
|
+
index = WorkspaceFileIndex(tmp_path)
|
|
102
|
+
results = _paths(index.search("apppy"))
|
|
103
|
+
assert results[0] == "cli/app.py"
|