Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Corentin André <corentin.andre@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com>
Co-authored-by: Peter Evers <pevers90@gmail.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: MichisGitIsKing <MichisGitIsKing@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-05-19 11:56:25 +02:00 committed by GitHub
parent 626f905186
commit 228f3c65a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
158 changed files with 7235 additions and 916 deletions

View file

@ -1,6 +1,5 @@
from __future__ import annotations
import os
from typing import Any, cast
from unittest.mock import AsyncMock, patch
@ -8,11 +7,11 @@ import httpx
import pytest
import respx
from tests.conftest import build_test_vibe_config
from tests.stubs.fake_connector_registry import FakeConnectorRegistry
from tests.stubs.fake_mcp_registry import FakeMCPRegistry
from vibe.core.config import ConnectorConfig, VibeConfig
from vibe.core.tools.base import BaseToolConfig, ToolError
from vibe.core.tools.connectors import CONNECTORS_ENV_VAR
from vibe.core.tools.connectors.connector_registry import (
ConnectorRegistry,
RemoteTool,
@ -153,22 +152,7 @@ class TestFakeConnectorRegistry:
class TestToolManagerConnectorIntegration:
@staticmethod
def _make_config(connectors: list[ConnectorConfig] | None = None) -> VibeConfig:
"""Minimal VibeConfig-like stub for ToolManager."""
return cast(
VibeConfig,
type(
"_Cfg",
(),
{
"mcp_servers": [],
"connectors": connectors or [],
"enabled_tools": [],
"disabled_tools": [],
"tools": {},
"tool_paths": [],
},
)(),
)
return build_test_vibe_config(connectors=connectors or [])
def test_connector_tools_registered(self) -> None:
registry = FakeConnectorRegistry(
@ -198,22 +182,6 @@ class TestToolManagerConnectorIntegration:
assert connector_tools == []
# ---------------------------------------------------------------------------
# ConnectorRegistry env var gating (tested via agent_loop helper logic)
# ---------------------------------------------------------------------------
class TestConnectorRegistryEnvGating:
def test_disabled_without_env_var(self) -> None:
with patch.dict(os.environ, {}, clear=False):
os.environ.pop(CONNECTORS_ENV_VAR, None)
assert os.getenv(CONNECTORS_ENV_VAR) != "1"
def test_enabled_with_env_var(self) -> None:
with patch.dict(os.environ, {CONNECTORS_ENV_VAR: "1"}):
assert os.getenv(CONNECTORS_ENV_VAR) == "1"
# ---------------------------------------------------------------------------
# Error message helpers
# ---------------------------------------------------------------------------
@ -339,7 +307,7 @@ class TestConnectorProxyToolRun:
mock_call.assert_awaited_once()
call_args = mock_call.call_args
assert "/v1/experimental/connectors/conn-123/mcp" in call_args.args[0]
assert "/v1/connectors-gateway/conn-123/mcp" in call_args.args[0]
assert call_args.args[1] == "search"
assert call_args.kwargs["headers"]["Authorization"] == "Bearer test-key"
@ -412,21 +380,7 @@ class TestConnectorProxyToolRun:
class TestConnectorDisableFiltering:
@staticmethod
def _make_config(connectors: list[ConnectorConfig] | None = None) -> VibeConfig:
return cast(
VibeConfig,
type(
"_Cfg",
(),
{
"mcp_servers": [],
"connectors": connectors or [],
"enabled_tools": [],
"disabled_tools": [],
"tools": {},
"tool_paths": [],
},
)(),
)
return build_test_vibe_config(connectors=connectors or [])
def test_disabled_connector_excludes_all_tools(self) -> None:
registry = FakeConnectorRegistry(

View file

@ -232,7 +232,7 @@ class TestAnswerHandling:
class TestPlanFile:
@pytest.mark.asyncio
async def test_content_passed_as_preview(
async def test_keybinding_hint_shown_as_preview(
self, tool: ExitPlanMode, plan_manager: MockAgentManager, tmp_path: Path
) -> None:
plan_file = tmp_path / "plan.md"
@ -247,21 +247,36 @@ class TestPlanFile:
)
await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert isinstance(cb.received_args, AskUserQuestionArgs)
assert cb.received_args.content_preview == "# My Plan\n\n- Step 1\n- Step 2\n"
assert cb.received_args.footer_note is not None
assert "Ctrl+G" in cb.received_args.footer_note
assert str(plan_file) in cb.received_args.footer_note
@pytest.mark.asyncio
async def test_missing_file_means_none_preview(
async def test_result_does_not_include_plan_content(
self, tool: ExitPlanMode, plan_manager: MockAgentManager, tmp_path: Path
) -> None:
plan_file = tmp_path / "nonexistent.md"
plan_file = tmp_path / "plan.md"
plan_file.write_text("# My Plan\n\n- Step 1\n- Step 2\n")
cb = MockCallback(AskUserQuestionResult(answers=[], cancelled=True))
cb = MockCallback(
AskUserQuestionResult(
answers=[
Answer(
question="q",
answer="Yes, and auto approve edits",
is_other=False,
)
],
cancelled=False,
)
)
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
user_input_callback=cb,
plan_file_path=plan_file,
)
await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert isinstance(cb.received_args, AskUserQuestionArgs)
assert cb.received_args.content_preview is None
result = await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert result.switched is True
assert "# My Plan" not in result.message
assert "source of truth" not in result.message

View file

@ -34,8 +34,8 @@ from vibe.core.tools.permissions import (
PermissionContext,
PermissionScope,
RequiredPermission,
wildcard_match,
)
from vibe.core.tools.utils import wildcard_match
class TestBashGranularPermissions:

View file

@ -10,6 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from pydantic import ValidationError
import pytest
from tests.conftest import build_test_vibe_config
from tests.stubs.fake_mcp_registry import FakeMCPRegistry
from vibe.core.config import MCPHttp, MCPStdio, MCPStreamableHttp, VibeConfig
from vibe.core.tools.mcp import (
@ -763,21 +764,7 @@ class TestMCPDisableFiltering:
def _make_config(
mcp_servers: list[MCPHttp | MCPStdio | MCPStreamableHttp] | None = None,
) -> VibeConfig:
return cast(
VibeConfig,
type(
"_Cfg",
(),
{
"mcp_servers": mcp_servers or [],
"connectors": [],
"enabled_tools": [],
"disabled_tools": [],
"tools": {},
"tool_paths": [],
},
)(),
)
return build_test_vibe_config(mcp_servers=mcp_servers or [])
def test_disabled_server_excludes_all_tools(self):
srv = MCPHttp(

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
from typing import TYPE_CHECKING, cast
from unittest.mock import AsyncMock, patch
from mistralai.client import Mistral
from mistralai.client.errors import SDKError
@ -13,12 +14,47 @@ from mistralai.client.models import (
)
import pytest
from tests.conftest import build_test_vibe_config
from tests.mock.utils import collect_result
from vibe.core.config import ProviderConfig
from vibe.core.config import ProviderConfig, VibeConfig
from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError
from vibe.core.tools.builtins.websearch import WebSearch, WebSearchArgs, WebSearchConfig
from vibe.core.tools.manager import ToolManager
from vibe.core.types import Backend
if TYPE_CHECKING:
from vibe.core.agents.manager import AgentManager
class InMemoryAgentManager:
def __init__(self, config: VibeConfig) -> None:
self.config = config
def _ctx_with_config(config: VibeConfig) -> InvokeContext:
return InvokeContext(
tool_call_id="t1",
agent_manager=cast("AgentManager", InMemoryAgentManager(config)),
)
def _mistral_provider(
api_key_env_var: str = "MISTRAL_API_KEY",
api_base: str = "https://on-prem.example.com/v1",
) -> ProviderConfig:
return ProviderConfig(
name="mistral",
api_base=api_base,
api_key_env_var=api_key_env_var,
backend=Backend.MISTRAL,
)
def _llamacpp_provider() -> ProviderConfig:
return ProviderConfig(
name="llamacpp", api_base="http://127.0.0.1:8080/v1", backend=Backend.GENERIC
)
def _make_response(
content: list | None = None, outputs: list | None = None
@ -109,6 +145,68 @@ async def test_run_missing_api_key(monkeypatch):
await collect_result(ws.run(WebSearchArgs(query="test")))
@pytest.mark.asyncio
async def test_run_uses_mistral_provider_api_key_env_var(monkeypatch):
monkeypatch.setenv("MISTRAL_API_KEY", "wrong-key")
monkeypatch.setenv("TEST_API_KEY", "provider-key")
config = WebSearchConfig()
ws = WebSearch(config_getter=lambda: config, state=BaseToolState())
ctx = _ctx_with_config(
build_test_vibe_config(providers=[_mistral_provider("TEST_API_KEY")])
)
response = _make_response(content=[TextChunk(text="The answer")])
with patch("vibe.core.tools.builtins.websearch.Mistral") as mistral_cls:
client = mistral_cls.return_value
client.__aenter__ = AsyncMock(return_value=client)
client.__aexit__ = AsyncMock(return_value=None)
client.beta.conversations.start_async = AsyncMock(return_value=response)
result = await collect_result(ws.run(WebSearchArgs(query="test query"), ctx))
assert result.answer == "The answer"
assert mistral_cls.call_args.kwargs["api_key"] == "provider-key"
assert mistral_cls.call_args.kwargs["server_url"] == "https://on-prem.example.com"
assert mistral_cls.call_args.kwargs["timeout_ms"] == 120000
@pytest.mark.asyncio
async def test_run_falls_back_to_default_api_key_env_var_when_provider_env_var_empty(
monkeypatch,
):
monkeypatch.setenv("MISTRAL_API_KEY", "fallback-key")
config = WebSearchConfig()
ws = WebSearch(config_getter=lambda: config, state=BaseToolState())
ctx = _ctx_with_config(build_test_vibe_config(providers=[_mistral_provider("")]))
response = _make_response(content=[TextChunk(text="The answer")])
with patch("vibe.core.tools.builtins.websearch.Mistral") as mistral_cls:
client = mistral_cls.return_value
client.__aenter__ = AsyncMock(return_value=client)
client.__aexit__ = AsyncMock(return_value=None)
client.beta.conversations.start_async = AsyncMock(return_value=response)
result = await collect_result(ws.run(WebSearchArgs(query="test query"), ctx))
assert result.answer == "The answer"
assert mistral_cls.call_args.kwargs["api_key"] == "fallback-key"
@pytest.mark.asyncio
async def test_run_reports_configured_api_key_env_var_when_missing(monkeypatch):
monkeypatch.setenv("MISTRAL_API_KEY", "fallback-key")
monkeypatch.setenv("TEST_API_KEY", "provider-key")
ctx = _ctx_with_config(
build_test_vibe_config(providers=[_mistral_provider("TEST_API_KEY")])
)
monkeypatch.delenv("TEST_API_KEY", raising=False)
config = WebSearchConfig()
ws = WebSearch(config_getter=lambda: config, state=BaseToolState())
with pytest.raises(ToolError, match="TEST_API_KEY"):
await collect_result(ws.run(WebSearchArgs(query="test"), ctx))
@pytest.mark.asyncio
async def test_run_returns_parsed_result(websearch):
response = _make_response(
@ -165,48 +263,23 @@ def test_resolve_server_url_no_agent_manager(websearch):
def test_resolve_server_url_with_mistral_provider(websearch):
config = MagicMock()
config.providers = [
ProviderConfig(
name="mistral",
api_base="https://on-prem.example.com/v1",
api_key_env_var="MISTRAL_API_KEY",
backend=Backend.MISTRAL,
)
]
agent_manager = MagicMock()
agent_manager.config = config
ctx = InvokeContext(tool_call_id="t1", agent_manager=agent_manager)
ctx = _ctx_with_config(build_test_vibe_config(providers=[_mistral_provider()]))
assert websearch._resolve_server_url(ctx) == "https://on-prem.example.com"
def test_resolve_server_url_with_default_provider(websearch):
config = MagicMock()
config.providers = [
ProviderConfig(
name="mistral",
api_base="https://api.mistral.ai/v1",
api_key_env_var="MISTRAL_API_KEY",
backend=Backend.MISTRAL,
ctx = _ctx_with_config(
build_test_vibe_config(
providers=[_mistral_provider(api_base="https://api.mistral.ai/v1")]
)
]
agent_manager = MagicMock()
agent_manager.config = config
ctx = InvokeContext(tool_call_id="t1", agent_manager=agent_manager)
)
assert websearch._resolve_server_url(ctx) == "https://api.mistral.ai"
def test_resolve_server_url_no_mistral_provider(websearch):
config = MagicMock()
config.providers = [
ProviderConfig(name="llamacpp", api_base="http://127.0.0.1:8080/v1")
]
agent_manager = MagicMock()
agent_manager.config = config
ctx = InvokeContext(tool_call_id="t1", agent_manager=agent_manager)
ctx = _ctx_with_config(
build_test_vibe_config(active_model="local", providers=[_llamacpp_provider()])
)
assert websearch._resolve_server_url(ctx) is None
@ -220,5 +293,86 @@ def test_is_available_without_key(monkeypatch):
assert WebSearch.is_available() is False
def test_is_available_uses_mistral_provider_api_key_env_var(monkeypatch):
monkeypatch.setenv("MISTRAL_API_KEY", "fallback-key")
monkeypatch.setenv("TEST_API_KEY", "provider-key")
config = build_test_vibe_config(providers=[_mistral_provider("TEST_API_KEY")])
monkeypatch.delenv("TEST_API_KEY", raising=False)
assert WebSearch.is_available(config) is False
monkeypatch.setenv("TEST_API_KEY", "provider-key")
assert WebSearch.is_available(config) is True
def test_is_available_uses_non_active_mistral_provider(monkeypatch):
monkeypatch.setenv("TEST_API_KEY", "provider-key")
config = build_test_vibe_config(
active_model="local",
providers=[_llamacpp_provider(), _mistral_provider("TEST_API_KEY")],
)
monkeypatch.delenv("TEST_API_KEY", raising=False)
assert WebSearch.is_available(config) is False
monkeypatch.setenv("TEST_API_KEY", "provider-key")
assert WebSearch.is_available(config) is True
def test_is_available_falls_back_to_default_api_key_env_var_without_mistral_provider(
monkeypatch,
):
monkeypatch.setenv("MISTRAL_API_KEY", "fallback-key")
config = build_test_vibe_config(
active_model="local", providers=[_llamacpp_provider()]
)
assert WebSearch.is_available(config) is True
monkeypatch.delenv("MISTRAL_API_KEY")
assert WebSearch.is_available(config) is False
def test_is_available_falls_back_to_default_api_key_env_var_when_provider_env_var_empty(
monkeypatch,
):
monkeypatch.setenv("MISTRAL_API_KEY", "fallback-key")
config = build_test_vibe_config(providers=[_mistral_provider("")])
assert WebSearch.is_available(config) is True
monkeypatch.delenv("MISTRAL_API_KEY")
assert WebSearch.is_available(config) is False
def test_tool_manager_websearch_availability_uses_provider_api_key_env_var(monkeypatch):
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
monkeypatch.setenv("TEST_API_KEY", "provider-key")
config = build_test_vibe_config(providers=[_mistral_provider("TEST_API_KEY")])
manager = ToolManager(lambda: config)
assert "web_search" in manager.available_tools
monkeypatch.delenv("TEST_API_KEY")
assert "web_search" not in manager.available_tools
def test_tool_manager_websearch_availability_falls_back_without_mistral_provider(
monkeypatch,
):
monkeypatch.setenv("MISTRAL_API_KEY", "fallback-key")
config = build_test_vibe_config(
active_model="local", providers=[_llamacpp_provider()]
)
manager = ToolManager(lambda: config)
assert "web_search" in manager.available_tools
monkeypatch.delenv("MISTRAL_API_KEY")
assert "web_search" not in manager.available_tools
def test_get_status_text():
assert WebSearch.get_status_text() == "Searching the web"

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from vibe.core.tools.utils import wildcard_match
from vibe.core.tools.permissions import wildcard_match
class TestWildcardMatch: