v2.10.0 (#697)
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:
parent
626f905186
commit
228f3c65a9
158 changed files with 7235 additions and 916 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue