v2.9.6 (#682)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
b23f49e5f4
commit
626f905186
59 changed files with 702 additions and 183 deletions
|
|
@ -19,16 +19,16 @@ class TestCloseSession:
|
|||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
session = acp_agent_loop.sessions[session_response.session_id]
|
||||
|
||||
backend_close = AsyncMock()
|
||||
backend_aexit = AsyncMock()
|
||||
telemetry_close = AsyncMock()
|
||||
cast(Any, session.agent_loop.backend).close = backend_close
|
||||
cast(Any, session.agent_loop.backend).__aexit__ = backend_aexit
|
||||
session.agent_loop.telemetry_client.aclose = telemetry_close
|
||||
|
||||
response = await acp_agent_loop.close_session(session_response.session_id)
|
||||
|
||||
assert response is not None
|
||||
assert session_response.session_id not in acp_agent_loop.sessions
|
||||
backend_close.assert_awaited_once()
|
||||
backend_aexit.assert_awaited_once_with(None, None, None)
|
||||
telemetry_close.assert_awaited_once()
|
||||
session_closed_events = [
|
||||
event
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.5"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.6"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -62,7 +62,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.5"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.6"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -432,12 +432,12 @@ class TestMistralRetry:
|
|||
with patch("vibe.core.llm.backend.mistral.Mistral") as mock_mistral_class:
|
||||
mock_mistral_class.return_value = MagicMock()
|
||||
backend._get_client()
|
||||
mock_mistral_class.assert_called_once_with(
|
||||
api_key=backend._api_key,
|
||||
server_url=backend._server_url,
|
||||
timeout_ms=720000,
|
||||
retry_config=backend._retry_config,
|
||||
)
|
||||
call_kwargs = mock_mistral_class.call_args.kwargs
|
||||
assert call_kwargs["api_key"] == backend._api_key
|
||||
assert call_kwargs["server_url"] == backend._server_url
|
||||
assert call_kwargs["timeout_ms"] == 720000
|
||||
assert call_kwargs["retry_config"] is backend._retry_config
|
||||
assert "async_client" in call_kwargs
|
||||
|
||||
|
||||
class TestMistralMapperPrepareMessage:
|
||||
|
|
|
|||
78
tests/core/test_build_ssl_context.py
Normal file
78
tests/core/test_build_ssl_context.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import ssl
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.utils.http import build_ssl_context
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_ssl_cache():
|
||||
build_ssl_context.cache_clear()
|
||||
yield
|
||||
build_ssl_context.cache_clear()
|
||||
|
||||
|
||||
def test_build_ssl_context_returns_ssl_context():
|
||||
ctx = build_ssl_context()
|
||||
assert isinstance(ctx, ssl.SSLContext)
|
||||
|
||||
|
||||
def test_build_ssl_context_loads_custom_cert_file(monkeypatch, tmp_path):
|
||||
cert_file = tmp_path / "custom.pem"
|
||||
cert_file.write_text("dummy")
|
||||
monkeypatch.setenv("SSL_CERT_FILE", str(cert_file))
|
||||
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
|
||||
|
||||
mock_ctx = MagicMock(spec=ssl.SSLContext)
|
||||
with patch(
|
||||
"vibe.core.utils.http.ssl.create_default_context", return_value=mock_ctx
|
||||
):
|
||||
build_ssl_context()
|
||||
|
||||
mock_ctx.load_verify_locations.assert_called_once_with(
|
||||
cafile=str(cert_file), capath=None
|
||||
)
|
||||
|
||||
|
||||
def test_build_ssl_context_loads_custom_cert_dir(monkeypatch, tmp_path):
|
||||
cert_dir = tmp_path / "certs"
|
||||
cert_dir.mkdir()
|
||||
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||
monkeypatch.setenv("SSL_CERT_DIR", str(cert_dir))
|
||||
|
||||
mock_ctx = MagicMock(spec=ssl.SSLContext)
|
||||
with patch(
|
||||
"vibe.core.utils.http.ssl.create_default_context", return_value=mock_ctx
|
||||
):
|
||||
build_ssl_context()
|
||||
|
||||
mock_ctx.load_verify_locations.assert_called_once_with(
|
||||
cafile=None, capath=str(cert_dir)
|
||||
)
|
||||
|
||||
|
||||
def test_build_ssl_context_warns_on_invalid_cert(monkeypatch, caplog):
|
||||
monkeypatch.setenv("SSL_CERT_FILE", "/nonexistent/path/cert.pem")
|
||||
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
|
||||
|
||||
ctx = build_ssl_context()
|
||||
assert isinstance(ctx, ssl.SSLContext)
|
||||
assert any(
|
||||
"Failed to load custom SSL certificates" in r.message for r in caplog.records
|
||||
)
|
||||
|
||||
|
||||
def test_build_ssl_context_no_custom_certs(monkeypatch):
|
||||
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
|
||||
|
||||
mock_ctx = MagicMock(spec=ssl.SSLContext)
|
||||
with patch(
|
||||
"vibe.core.utils.http.ssl.create_default_context", return_value=mock_ctx
|
||||
):
|
||||
build_ssl_context()
|
||||
|
||||
mock_ctx.load_verify_locations.assert_not_called()
|
||||
|
|
@ -293,6 +293,68 @@ class TestMigrateLeavesFindInBashAllowlist:
|
|||
assert "tools" not in result
|
||||
|
||||
|
||||
class TestMigrateStripsBashAllowlistWildcardSuffix:
|
||||
def test_strips_trailing_wildcard_from_bash_allowlist_entries(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||
config_file = tmp_path / "config.toml"
|
||||
data = {
|
||||
"tools": {"bash": {"allowlist": ["git commit *", "npm install *", "echo"]}}
|
||||
}
|
||||
with config_file.open("wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
|
||||
reset_harness_files_manager()
|
||||
init_harness_files_manager("user")
|
||||
VibeConfig._migrate()
|
||||
|
||||
with config_file.open("rb") as f:
|
||||
result = tomllib.load(f)
|
||||
assert result["tools"]["bash"]["allowlist"] == [
|
||||
"echo",
|
||||
"find",
|
||||
"git commit",
|
||||
"npm install",
|
||||
]
|
||||
|
||||
def test_dedupes_when_stripping_collides_with_existing_entry(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||
config_file = tmp_path / "config.toml"
|
||||
data = {
|
||||
"tools": {"bash": {"allowlist": ["git commit *", "git commit", "find"]}}
|
||||
}
|
||||
with config_file.open("wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
|
||||
reset_harness_files_manager()
|
||||
init_harness_files_manager("user")
|
||||
VibeConfig._migrate()
|
||||
|
||||
with config_file.open("rb") as f:
|
||||
result = tomllib.load(f)
|
||||
assert result["tools"]["bash"]["allowlist"] == ["find", "git commit"]
|
||||
|
||||
def test_noop_when_no_wildcard_suffix_present(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||
config_file = tmp_path / "config.toml"
|
||||
data = {"tools": {"bash": {"allowlist": ["echo", "find", "ls"]}}}
|
||||
with config_file.open("wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
|
||||
reset_harness_files_manager()
|
||||
init_harness_files_manager("user")
|
||||
VibeConfig._migrate()
|
||||
|
||||
with config_file.open("rb") as f:
|
||||
result = tomllib.load(f)
|
||||
assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]
|
||||
|
||||
|
||||
class TestMigrateMistralVibeCliLatestDefaults:
|
||||
def test_updates_alias_temperature_and_thinking_for_default_model(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
|
|
|
|||
|
|
@ -20,3 +20,6 @@ class FakeTranscribeClient:
|
|||
) -> AsyncIterator[TranscribeEvent]:
|
||||
for event in self._events:
|
||||
yield event
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ class FakeVoiceManager:
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
def _set_state(self, state: TranscribeState) -> None:
|
||||
if self._transcribe_state == state:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from tests.stubs.fake_backend import FakeBackend
|
|||
from vibe.core.agents.manager import AgentManager
|
||||
from vibe.core.agents.models import (
|
||||
BUILTIN_AGENTS,
|
||||
CHAT,
|
||||
AgentProfile,
|
||||
AgentSafety,
|
||||
AgentType,
|
||||
|
|
@ -188,6 +189,49 @@ class TestAgentApplyToConfig:
|
|||
"exit_plan_mode",
|
||||
}
|
||||
|
||||
def test_base_disabled_tools_are_filtered_from_profile_enabled_tools(self) -> None:
|
||||
base = VibeConfig(
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
disabled_tools=["ask_user_question"],
|
||||
)
|
||||
|
||||
result = CHAT.apply_to_config(base)
|
||||
|
||||
assert "ask_user_question" not in result.enabled_tools
|
||||
assert "grep" in result.enabled_tools
|
||||
assert "read_file" in result.enabled_tools
|
||||
assert "task" in result.enabled_tools
|
||||
|
||||
def test_base_disabled_tools_filter_supports_glob_patterns(self) -> None:
|
||||
base = VibeConfig(
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
disabled_tools=["ask_*"],
|
||||
)
|
||||
agent = AgentProfile(
|
||||
name="custom",
|
||||
display_name="Custom",
|
||||
description="",
|
||||
safety=AgentSafety.NEUTRAL,
|
||||
overrides={"enabled_tools": ["grep", "ask_user_question", "ask_extra"]},
|
||||
)
|
||||
|
||||
result = agent.apply_to_config(base)
|
||||
|
||||
assert result.enabled_tools == ["grep"]
|
||||
|
||||
def test_empty_base_disabled_tools_leaves_enabled_tools_untouched(self) -> None:
|
||||
base = VibeConfig(
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
disabled_tools=[],
|
||||
)
|
||||
|
||||
result = CHAT.apply_to_config(base)
|
||||
|
||||
assert "ask_user_question" in result.enabled_tools
|
||||
|
||||
def test_custom_prompt_found_in_global_when_missing_from_project(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class TestApproveAlwaysPermanentWithGranularPermissions:
|
|||
agent.approve_always("bash", perms, save_permanently=True)
|
||||
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["npm install *"]
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["npm install"]
|
||||
|
||||
def test_also_adds_session_rules(self, config_dir: Path):
|
||||
agent = build_test_agent_loop()
|
||||
|
|
@ -75,9 +75,7 @@ class TestApproveAlwaysPermanentWithGranularPermissions:
|
|||
assert "bash" not in persisted.get("tools", {})
|
||||
|
||||
def test_does_not_duplicate_existing_allowlist_entries(self, config_dir: Path):
|
||||
config = build_test_vibe_config(
|
||||
tools={"bash": {"allowlist": ["npm install *"]}}
|
||||
)
|
||||
config = build_test_vibe_config(tools={"bash": {"allowlist": ["npm install"]}})
|
||||
agent = build_test_agent_loop(config=config)
|
||||
perms = self._make_permissions()
|
||||
|
||||
|
|
@ -88,14 +86,14 @@ class TestApproveAlwaysPermanentWithGranularPermissions:
|
|||
assert persisted.get("tools", {}).get("bash", {}).get("allowlist") is None
|
||||
|
||||
def test_appends_new_patterns_to_existing_allowlist(self, config_dir: Path):
|
||||
config = build_test_vibe_config(tools={"bash": {"allowlist": ["git *"]}})
|
||||
config = build_test_vibe_config(tools={"bash": {"allowlist": ["git"]}})
|
||||
agent = build_test_agent_loop(config=config)
|
||||
perms = self._make_permissions()
|
||||
|
||||
agent.approve_always("bash", perms, save_permanently=True)
|
||||
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["git *", "npm install *"]
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["git", "npm install"]
|
||||
|
||||
def test_multiple_permissions_persisted(self, config_dir: Path):
|
||||
agent = build_test_agent_loop()
|
||||
|
|
@ -117,4 +115,4 @@ class TestApproveAlwaysPermanentWithGranularPermissions:
|
|||
agent.approve_always("bash", perms, save_permanently=True)
|
||||
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["/tmp/*", "npm install *"]
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["/tmp/*", "npm install"]
|
||||
|
|
|
|||
|
|
@ -329,6 +329,35 @@ class TestReadOnlyAgentMiddleware:
|
|||
PLAN_REMINDER_SNIPPET = "Plan mode is active"
|
||||
|
||||
|
||||
class TestMakePlanAgentReminder:
|
||||
def test_default_includes_both_interactive_tool_instructions(self) -> None:
|
||||
reminder = make_plan_agent_reminder("/tmp/test-plan.md")
|
||||
assert "ask_user_question" in reminder
|
||||
assert "exit_plan_mode" in reminder
|
||||
|
||||
def test_omits_ask_user_question_when_tool_unavailable(self) -> None:
|
||||
reminder = make_plan_agent_reminder(
|
||||
"/tmp/test-plan.md", has_ask_user_question=False
|
||||
)
|
||||
assert "ask_user_question" not in reminder
|
||||
assert "exit_plan_mode" in reminder
|
||||
|
||||
def test_omits_exit_plan_mode_when_tool_unavailable(self) -> None:
|
||||
reminder = make_plan_agent_reminder(
|
||||
"/tmp/test-plan.md", has_exit_plan_mode=False
|
||||
)
|
||||
assert "exit_plan_mode" not in reminder
|
||||
assert "tell them to switch modes" in reminder
|
||||
|
||||
def test_omits_both_when_neither_available(self) -> None:
|
||||
reminder = make_plan_agent_reminder(
|
||||
"/tmp/test-plan.md", has_ask_user_question=False, has_exit_plan_mode=False
|
||||
)
|
||||
assert "ask_user_question" not in reminder
|
||||
assert "exit_plan_mode" not in reminder
|
||||
assert "Plan mode is active" in reminder
|
||||
|
||||
|
||||
class TestMiddlewarePipelineWithReadOnlyAgent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_pipeline_includes_injection(self, ctx: ConversationContext) -> None:
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ class TestStopRecording:
|
|||
return
|
||||
yield # makes this an async generator
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
recorder = FakeAudioRecorder()
|
||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||
manager = VoiceManager(
|
||||
|
|
@ -218,6 +221,9 @@ class TestCancelRecording:
|
|||
return
|
||||
yield
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
recorder = FakeAudioRecorder()
|
||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||
manager = VoiceManager(
|
||||
|
|
@ -388,6 +394,9 @@ class TestTranscription:
|
|||
raise RuntimeError("network error")
|
||||
yield # makes this an async generator
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
recorder = FakeAudioRecorder()
|
||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||
manager = VoiceManager(
|
||||
|
|
@ -488,6 +497,9 @@ class TestTelemetryTracking:
|
|||
raise RuntimeError("network error")
|
||||
yield
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
recorder = FakeAudioRecorder()
|
||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||
mock_telemetry = MagicMock()
|
||||
|
|
@ -556,6 +568,9 @@ class TestTelemetryTracking:
|
|||
return
|
||||
yield
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
recorder = FakeAudioRecorder()
|
||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||
mock_telemetry = MagicMock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue