Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Hdandria <henri.dandria@mistral.ai> Co-authored-by: Ivana Dunisijevic <ivana.dunisijevic@mistral.ai> Co-authored-by: Jean Burellier <sheplu@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Mert Unsal <mert.unsal@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: renovate-mistral[bot] <253709520+renovate-mistral[bot]@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
267 lines
9.2 KiB
Python
267 lines
9.2 KiB
Python
from __future__ import annotations
|
|
|
|
from acp.schema import ToolCallStart
|
|
|
|
from vibe.acp.tools.builtins.task import Task as AcpTask
|
|
from vibe.acp.tools.builtins.todo import Todo as AcpTodo, TodoArgs
|
|
from vibe.acp.user_display_content import USER_DISPLAY_CONTENT_META_KEY
|
|
from vibe.acp.utils import (
|
|
TOOL_OPTIONS,
|
|
ToolOption,
|
|
build_permission_options,
|
|
create_tool_call_replay,
|
|
create_tool_result_replay,
|
|
create_user_message_replay,
|
|
get_proxy_help_text,
|
|
tool_call_replay_update,
|
|
)
|
|
from vibe.core.llm.format import ResolvedToolCall
|
|
from vibe.core.paths import GLOBAL_ENV_FILE
|
|
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS
|
|
from vibe.core.tools.builtins.task import TaskArgs
|
|
from vibe.core.tools.permissions import PermissionScope, RequiredPermission
|
|
from vibe.core.types import (
|
|
FunctionCall,
|
|
LLMMessage,
|
|
Role,
|
|
ToolCall,
|
|
UserDisplayContentMetadata,
|
|
)
|
|
|
|
|
|
def _write_env_file(content: str) -> None:
|
|
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
|
GLOBAL_ENV_FILE.path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
class TestGetProxyHelpText:
|
|
def test_returns_string(self) -> None:
|
|
result = get_proxy_help_text()
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_includes_proxy_configuration_header(self) -> None:
|
|
result = get_proxy_help_text()
|
|
|
|
assert "## Proxy Configuration" in result
|
|
|
|
def test_includes_usage_section(self) -> None:
|
|
result = get_proxy_help_text()
|
|
|
|
assert "### Usage:" in result
|
|
assert "/proxy-setup" in result
|
|
|
|
def test_includes_all_supported_variables(self) -> None:
|
|
result = get_proxy_help_text()
|
|
|
|
for key in SUPPORTED_PROXY_VARS:
|
|
assert f"`{key}`" in result
|
|
|
|
def test_shows_none_configured_when_no_settings(self) -> None:
|
|
result = get_proxy_help_text()
|
|
|
|
assert "(none configured)" in result
|
|
|
|
def test_shows_current_settings_when_configured(self) -> None:
|
|
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
|
|
|
result = get_proxy_help_text()
|
|
|
|
assert "HTTP_PROXY=http://proxy:8080" in result
|
|
assert "(none configured)" not in result
|
|
|
|
def test_shows_only_set_values(self) -> None:
|
|
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
|
|
|
result = get_proxy_help_text()
|
|
|
|
assert "HTTP_PROXY=http://proxy:8080" in result
|
|
assert "HTTPS_PROXY=" not in result
|
|
|
|
|
|
class TestBuildPermissionOptions:
|
|
def test_no_permissions_returns_default_options(self) -> None:
|
|
result = build_permission_options(None)
|
|
assert result is TOOL_OPTIONS
|
|
|
|
def test_empty_list_returns_default_options(self) -> None:
|
|
result = build_permission_options([])
|
|
assert result is TOOL_OPTIONS
|
|
|
|
def test_with_permissions_includes_labels_in_allow_always(self) -> None:
|
|
permissions = [
|
|
RequiredPermission(
|
|
scope=PermissionScope.COMMAND_PATTERN,
|
|
invocation_pattern="npm install foo",
|
|
session_pattern="npm install *",
|
|
label="npm install *",
|
|
)
|
|
]
|
|
result = build_permission_options(permissions)
|
|
|
|
assert len(result) == 4
|
|
allow_always = next(o for o in result if o.option_id == ToolOption.ALLOW_ALWAYS)
|
|
assert "session" in allow_always.name.lower()
|
|
allow_permanent = next(
|
|
o for o in result if o.option_id == ToolOption.ALLOW_ALWAYS_PERMANENT
|
|
)
|
|
assert "Always allow" == allow_permanent.name
|
|
|
|
def test_allow_always_has_field_meta(self) -> None:
|
|
permissions = [
|
|
RequiredPermission(
|
|
scope=PermissionScope.COMMAND_PATTERN,
|
|
invocation_pattern="mkdir foo",
|
|
session_pattern="mkdir *",
|
|
label="mkdir *",
|
|
)
|
|
]
|
|
result = build_permission_options(permissions)
|
|
|
|
allow_always = next(o for o in result if o.option_id == ToolOption.ALLOW_ALWAYS)
|
|
assert allow_always.field_meta is not None
|
|
assert "required_permissions" in allow_always.field_meta
|
|
meta_perms = allow_always.field_meta["required_permissions"]
|
|
assert len(meta_perms) == 1
|
|
assert meta_perms[0]["session_pattern"] == "mkdir *"
|
|
|
|
def test_allow_once_and_reject_unchanged(self) -> None:
|
|
permissions = [
|
|
RequiredPermission(
|
|
scope=PermissionScope.URL_PATTERN,
|
|
invocation_pattern="example.com",
|
|
session_pattern="example.com",
|
|
label="fetching from example.com",
|
|
)
|
|
]
|
|
result = build_permission_options(permissions)
|
|
|
|
allow_once = next(o for o in result if o.option_id == ToolOption.ALLOW_ONCE)
|
|
reject_once = next(o for o in result if o.option_id == ToolOption.REJECT_ONCE)
|
|
assert allow_once.name == "Allow once"
|
|
assert reject_once.name == "Deny"
|
|
assert allow_once.field_meta is None
|
|
assert reject_once.field_meta is None
|
|
|
|
|
|
class TestCreateUserMessageReplay:
|
|
def test_replays_plain_text_without_field_meta(self) -> None:
|
|
replay = create_user_message_replay(
|
|
LLMMessage(role=Role.user, content="Hello", message_id="msg-1")
|
|
)
|
|
|
|
assert replay.content.text == "Hello"
|
|
assert replay.message_id == "msg-1"
|
|
assert replay.field_meta is None
|
|
|
|
def test_replays_user_display_content_as_field_meta(self) -> None:
|
|
user_display_content = UserDisplayContentMetadata(
|
|
version="1.0.0",
|
|
host="mistral-vscode",
|
|
content=[
|
|
{"type": "text", "text": "Look at "},
|
|
{
|
|
"type": "workspace_mention",
|
|
"kind": "file",
|
|
"uri": "file:///repo/src/app.ts",
|
|
"name": "app.ts",
|
|
},
|
|
],
|
|
)
|
|
|
|
replay = create_user_message_replay(
|
|
LLMMessage(
|
|
role=Role.user,
|
|
content="Look at app.ts",
|
|
message_id="msg-1",
|
|
user_display_content=user_display_content,
|
|
)
|
|
)
|
|
|
|
assert replay.content.text == "Look at app.ts"
|
|
assert replay.field_meta == {
|
|
USER_DISPLAY_CONTENT_META_KEY: user_display_content.model_dump(mode="json")
|
|
}
|
|
|
|
|
|
class TestCreateToolCallReplay:
|
|
def test_carries_status_and_resolved_kind(self) -> None:
|
|
update = create_tool_call_replay("call_1", "grep", '{"pattern": "foo"}')
|
|
|
|
assert update.status == "completed"
|
|
assert update.kind == "search"
|
|
assert update.field_meta == {"tool_name": "grep"}
|
|
assert update.raw_input == '{"pattern": "foo"}'
|
|
|
|
def test_unknown_tool_defaults_kind_to_other(self) -> None:
|
|
update = create_tool_call_replay("call_2", "mystery_tool", None)
|
|
|
|
assert update.status == "completed"
|
|
assert update.kind == "other"
|
|
assert update.field_meta == {"tool_name": "mystery_tool"}
|
|
|
|
|
|
class TestCreateToolResultReplay:
|
|
def test_carries_kind_and_tool_name_meta(self) -> None:
|
|
msg = LLMMessage(
|
|
role=Role.tool, tool_call_id="call_1", name="bash", content="exit 0"
|
|
)
|
|
|
|
update = create_tool_result_replay(msg)
|
|
|
|
assert update is not None
|
|
assert update.kind == "execute"
|
|
assert update.status == "completed"
|
|
assert update.field_meta == {"tool_name": "bash"}
|
|
|
|
def test_returns_none_without_tool_call_id(self) -> None:
|
|
msg = LLMMessage(role=Role.tool, name="bash", content="exit 0")
|
|
|
|
assert create_tool_result_replay(msg) is None
|
|
|
|
|
|
def _tool_call(call_id: str, name: str, arguments: str | None) -> ToolCall:
|
|
return ToolCall(id=call_id, function=FunctionCall(name=name, arguments=arguments))
|
|
|
|
|
|
class TestToolCallReplayUpdate:
|
|
def test_resolved_task_carries_agent_and_task_meta(self) -> None:
|
|
tool_call = _tool_call(
|
|
"call_1", "task", '{"agent": "explore", "task": "find the bug"}'
|
|
)
|
|
resolved = ResolvedToolCall(
|
|
tool_name="task",
|
|
tool_class=AcpTask,
|
|
validated_args=TaskArgs(agent="explore", task="find the bug"),
|
|
call_id="call_1",
|
|
)
|
|
|
|
update = tool_call_replay_update(resolved, tool_call)
|
|
|
|
assert isinstance(update, ToolCallStart)
|
|
assert update.status == "completed"
|
|
assert update.field_meta is not None
|
|
assert update.field_meta["tool_name"] == "task"
|
|
assert update.field_meta["agent"] == "explore"
|
|
assert update.field_meta["task"] == "find the bug"
|
|
|
|
def test_unresolved_call_falls_back_to_generic_replay(self) -> None:
|
|
tool_call = _tool_call("call_2", "grep", '{"pattern": "foo"}')
|
|
|
|
update = tool_call_replay_update(None, tool_call)
|
|
|
|
assert isinstance(update, ToolCallStart)
|
|
assert update.status == "completed"
|
|
assert update.kind == "search"
|
|
assert update.field_meta == {"tool_name": "grep"}
|
|
|
|
def test_hidden_tool_call_is_skipped(self) -> None:
|
|
tool_call = _tool_call("call_3", "todo", "{}")
|
|
resolved = ResolvedToolCall(
|
|
tool_name="todo",
|
|
tool_class=AcpTodo,
|
|
validated_args=TodoArgs(action="read"),
|
|
call_id="call_3",
|
|
)
|
|
|
|
assert tool_call_replay_update(resolved, tool_call) is None
|