Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Jean-Baptiste Muscat <jeanbaptiste.muscatdupuis@mistral.ai>
Co-authored-by: JeroenvdV <JeroenvdV@users.noreply.github.com>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@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: allansimon-mistral <allan.simon@ext.mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Clément Sirieix 2026-05-05 14:40:11 +02:00 committed by GitHub
parent 71f373c60c
commit 4972dd5694
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 2892 additions and 842 deletions

View file

@ -565,7 +565,7 @@ async def start_session_with_request_permission(
assert isinstance(last_response, RequestPermissionJsonRpcRequest)
assert last_response.params is not None
assert len(last_response.params.options) == 3
assert len(last_response.params.options) == 4
return last_response
@ -778,13 +778,15 @@ class TestToolCallStructure:
)
assert permission_request.params is not None
# Verify "Allow always" option includes the pattern label
# Verify granular permissions are passed in field_meta
allow_always = next(
o
for o in permission_request.params.options
if o.option_id == ToolOption.ALLOW_ALWAYS
)
assert "npm install *" in allow_always.name
assert allow_always.name == "Allow for remainder of this session"
assert allow_always.field_meta is not None
assert "required_permissions" in allow_always.field_meta
@pytest.mark.skip(reason="Long running tool call updates are not implemented yet")
@pytest.mark.asyncio

View file

@ -34,7 +34,7 @@ class TestACPInitialize:
),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.3"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.4"
)
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.3"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.4"
)
assert response.auth_methods is not None

View file

@ -0,0 +1,363 @@
from __future__ import annotations
import json
from pathlib import Path
from acp.schema import SessionInfoUpdate
import pytest
import tomli_w
from tests.conftest import build_test_vibe_config, get_base_config
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.acp.exceptions import InternalError, InvalidRequestError, SessionNotFoundError
from vibe.core.agent_loop import AgentLoop
from vibe.core.config import ModelConfig, SessionLoggingConfig
@pytest.fixture
def acp_agent_with_session_config(
backend: FakeBackend, temp_session_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> tuple[VibeAcpAgentLoop, FakeClient]:
session_config = SessionLoggingConfig(
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
)
config = build_test_vibe_config(
active_model="devstral-latest",
models=[
ModelConfig(
name="devstral-latest", provider="mistral", alias="devstral-latest"
)
],
session_logging=session_config,
)
class PatchedAgentLoop(AgentLoop):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **{**kwargs, "backend": backend})
self._base_config = config
self.agent_manager.invalidate_config()
monkeypatch.setattr("vibe.acp.acp_agent_loop.AgentLoop", PatchedAgentLoop)
monkeypatch.setattr(VibeAcpAgentLoop, "_load_config", lambda self: config)
monkeypatch.setattr(
"vibe.acp.acp_agent_loop.VibeConfig.load", lambda *args, **kwargs: config
)
vibe_acp_agent = VibeAcpAgentLoop()
client = FakeClient()
vibe_acp_agent.on_connect(client)
client.on_connect(vibe_acp_agent)
return vibe_acp_agent, client
class TestSessionSetTitle:
@pytest.mark.asyncio
async def test_updates_live_unsaved_session_title(
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
) -> None:
acp_agent, client = acp_agent_with_session_config
response = await acp_agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
assert response is not None
result = await acp_agent.ext_method(
"session/set_title",
{"sessionId": response.session_id, "title": "Manual title"},
)
assert result == {}
session = acp_agent.sessions[response.session_id]
metadata = session.agent_loop.session_logger.session_metadata
assert metadata is not None
assert metadata.title == "Manual title"
assert metadata.title_source == "manual"
assert metadata.end_time is None
assert not session.agent_loop.session_logger.metadata_filepath.exists()
info_updates = [
notification.update
for notification in client._session_updates
if isinstance(notification.update, SessionInfoUpdate)
]
assert len(info_updates) == 1
assert info_updates[0].title == "Manual title"
assert info_updates[0].updated_at is None
@pytest.mark.asyncio
async def test_updates_live_saved_session_title(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
saved_session_id = "saved-session-12345678"
acp_session_id = saved_session_id[:8]
cwd = str(Path.cwd())
session_dir = create_test_session(
temp_session_dir,
saved_session_id,
cwd,
title="Old title",
end_time="2024-01-01T12:05:00Z",
)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=acp_session_id)
client._session_updates.clear()
result = await acp_agent.ext_method(
"session/set_title",
{"sessionId": saved_session_id, "title": "Renamed session"},
)
assert result == {}
session = acp_agent.sessions[acp_session_id]
metadata = session.agent_loop.session_logger.session_metadata
assert metadata is not None
assert metadata.title == "Renamed session"
assert metadata.title_source == "manual"
assert metadata.end_time == "2024-01-01T12:05:00Z"
assert session.agent_loop.session_id == saved_session_id
saved_metadata = json.loads((session_dir / "meta.json").read_text())
assert saved_metadata["title"] == "Renamed session"
assert saved_metadata["title_source"] == "manual"
assert saved_metadata["end_time"] == "2024-01-01T12:05:00Z"
info_updates = [
notification
for notification in client._session_updates
if isinstance(notification.update, SessionInfoUpdate)
]
assert len(info_updates) == 1
assert info_updates[0].session_id == acp_session_id
assert info_updates[0].update.title == "Renamed session"
assert info_updates[0].update.updated_at == metadata.end_time
assert saved_metadata["end_time"] == info_updates[0].update.updated_at
@pytest.mark.asyncio
async def test_loaded_session_title_is_unchanged_when_persist_fails(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
monkeypatch: pytest.MonkeyPatch,
) -> None:
acp_agent, client = acp_agent_with_session_config
saved_session_id = "saved-session-12345678"
acp_session_id = saved_session_id[:8]
cwd = str(Path.cwd())
create_test_session(
temp_session_dir,
saved_session_id,
cwd,
title="Old title",
end_time="2024-01-01T12:05:00Z",
)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=acp_session_id)
client._session_updates.clear()
async def fail_persist(*args, **kwargs):
raise ValueError("Cannot rewrite metadata")
monkeypatch.setattr(
"vibe.acp.acp_agent_loop.update_saved_session_title_at_path", fail_persist
)
with pytest.raises(InternalError):
await acp_agent.ext_method(
"session/set_title",
{"sessionId": saved_session_id, "title": "Renamed session"},
)
session = acp_agent.sessions[acp_session_id]
metadata = session.agent_loop.session_logger.session_metadata
assert metadata is not None
assert metadata.title == "Old title"
assert metadata.title_source == "auto"
assert not [
notification
for notification in client._session_updates
if isinstance(notification.update, SessionInfoUpdate)
]
@pytest.mark.asyncio
async def test_updates_saved_but_not_loaded_session_title(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
session_id = "offline-session-12345678"
cwd = str(Path.cwd())
session_dir = create_test_session(
temp_session_dir,
session_id,
cwd,
title="Old title",
end_time="2024-01-01T12:05:00Z",
)
result = await acp_agent.ext_method(
"session/set_title", {"sessionId": session_id, "title": "Renamed session"}
)
assert result == {}
saved_metadata = json.loads((session_dir / "meta.json").read_text())
assert saved_metadata["title"] == "Renamed session"
assert saved_metadata["title_source"] == "manual"
info_updates = [
notification
for notification in client._session_updates
if isinstance(notification.update, SessionInfoUpdate)
]
assert len(info_updates) == 1
assert info_updates[0].session_id == session_id
assert info_updates[0].update.title == "Renamed session"
assert info_updates[0].update.updated_at == saved_metadata["end_time"]
@pytest.mark.asyncio
async def test_updates_saved_session_with_configured_log_dir_without_api_key(
self,
config_dir: Path,
temp_session_dir: Path,
create_test_session,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config = {
**get_base_config(),
"session_logging": {
"enabled": True,
"save_dir": str(temp_session_dir),
"session_prefix": "session",
},
}
monkeypatch.setenv("VIBE_HOME", str(config_dir))
(config_dir / "config.toml").write_text(tomli_w.dumps(config), encoding="utf-8")
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
session_id = "offline-session-12345678"
session_dir = create_test_session(
temp_session_dir,
session_id,
str(Path.cwd()),
title="Old title",
end_time="2024-01-01T12:05:00Z",
)
acp_agent = VibeAcpAgentLoop()
client = FakeClient()
acp_agent.on_connect(client)
client.on_connect(acp_agent)
result = await acp_agent.ext_method(
"session/set_title",
{"sessionId": session_id, "title": "Renamed without key"},
)
assert result == {}
saved_metadata = json.loads((session_dir / "meta.json").read_text())
assert saved_metadata["title"] == "Renamed without key"
assert saved_metadata["title_source"] == "manual"
info_updates = [
notification
for notification in client._session_updates
if isinstance(notification.update, SessionInfoUpdate)
]
assert len(info_updates) == 1
assert info_updates[0].session_id == session_id
assert info_updates[0].update.title == "Renamed without key"
@pytest.mark.asyncio
async def test_raises_on_invalid_params(
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
) -> None:
acp_agent, _client = acp_agent_with_session_config
with pytest.raises(InvalidRequestError):
await acp_agent.ext_method(
"session/set_title", {"title": "Missing session id"}
)
with pytest.raises(InvalidRequestError):
await acp_agent.ext_method(
"session/set_title", {"sessionId": "missing-title-session"}
)
with pytest.raises(InvalidRequestError):
await acp_agent.ext_method(
"session/set_title",
{"sessionId": "blank-title-session", "title": " "},
)
with pytest.raises(InvalidRequestError):
await acp_agent.ext_method(
"session/set_title", {"sessionId": " ", "title": "Blank session id"}
)
with pytest.raises(InvalidRequestError):
await acp_agent.ext_method(
"session/set_title",
{"savedSessionId": "saved-session", "title": "Unsupported target"},
)
@pytest.mark.asyncio
async def test_session_id_falls_back_to_saved_session_lookup(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
session_id = "saved-session-12345678"
cwd = str(Path.cwd())
session_dir = create_test_session(
temp_session_dir,
session_id,
cwd,
title="Old title",
end_time="2024-01-01T12:05:00Z",
)
result = await acp_agent.ext_method(
"session/set_title", {"sessionId": session_id, "title": "Renamed session"}
)
assert result == {}
saved_metadata = json.loads((session_dir / "meta.json").read_text())
assert saved_metadata["title"] == "Renamed session"
assert saved_metadata["title_source"] == "manual"
info_updates = [
notification
for notification in client._session_updates
if isinstance(notification.update, SessionInfoUpdate)
]
assert len(info_updates) == 1
assert info_updates[0].session_id == session_id
assert info_updates[0].update.title == "Renamed session"
@pytest.mark.asyncio
async def test_raises_when_session_cannot_be_found(
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
) -> None:
acp_agent, _client = acp_agent_with_session_config
with pytest.raises(SessionNotFoundError):
await acp_agent.ext_method(
"session/set_title",
{"sessionId": "missing-session", "title": "Renamed session"},
)

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
from unittest.mock import patch
import pytest
@ -42,6 +43,37 @@ class TestTelemetryNotification:
assert telemetry_events == []
@pytest.mark.asyncio
async def test_at_mention_inserted_dispatches_telemetry(
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict[str, Any]]
) -> None:
session = await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
telemetry_events.clear()
await acp_agent_loop.ext_notification(
"telemetry/send",
{
"event": "vibe.at_mention_inserted",
"session_id": session.session_id,
"properties": {
"nb_mentions": 2,
"context_types": {"file": 1, "folder": 1},
"file_extensions": {".py": 1},
"message_id": "msg-abc",
},
},
)
at_mention_events = [
e for e in telemetry_events if e["event_name"] == "vibe.at_mention_inserted"
]
assert len(at_mention_events) == 1
props = at_mention_events[0]["properties"]
assert props["nb_mentions"] == 2
assert props["context_types"] == {"file": 1, "folder": 1}
assert props["file_extensions"] == {".py": 1}
assert props["message_id"] == "msg-abc"
@pytest.mark.asyncio
async def test_raises_on_invalid_params(
self, acp_agent_loop: VibeAcpAgentLoop

View file

@ -81,10 +81,13 @@ class TestBuildPermissionOptions:
]
result = build_permission_options(permissions)
assert len(result) == 3
assert len(result) == 4
allow_always = next(o for o in result if o.option_id == ToolOption.ALLOW_ALWAYS)
assert "npm install *" in allow_always.name
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 = [
@ -118,6 +121,6 @@ class TestBuildPermissionOptions:
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 == "Reject once"
assert reject_once.name == "Deny"
assert allow_once.field_meta is None
assert reject_once.field_meta is None