v2.3.0 (#429)
Co-authored-by: Carlo <carloantonio.patti@mistral.ai> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Clement Sirieix <clem.sirieix@gmail.com> Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Vincent Guilloux <vincent.guilloux@mistral.ai> Co-authored-by: Thomas Kenbeek <thomas.kenbeek@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
a560a47ce8
commit
5d2e01a6d7
139 changed files with 7152 additions and 1457 deletions
|
|
@ -28,7 +28,7 @@ class TestACPInitialize:
|
|||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.2.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.3.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -52,7 +52,7 @@ class TestACPInitialize:
|
|||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.2.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.3.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ def acp_agent_with_session_config(
|
|||
models=[
|
||||
ModelConfig(
|
||||
name="devstral-latest", provider="mistral", alias="devstral-latest"
|
||||
)
|
||||
),
|
||||
ModelConfig(
|
||||
name="devstral-small", provider="mistral", alias="devstral-small"
|
||||
),
|
||||
],
|
||||
session_logging=session_config,
|
||||
)
|
||||
|
|
@ -58,7 +61,7 @@ def acp_agent_with_session_config(
|
|||
|
||||
class TestLoadSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_returns_response_with_models_and_modes(
|
||||
async def test_load_session_response_structure(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
|
|
@ -76,9 +79,49 @@ class TestLoadSession:
|
|||
|
||||
assert response is not None
|
||||
assert response.models is not None
|
||||
assert len(response.models.available_models) == 2
|
||||
|
||||
assert response.models.current_model_id == "devstral-latest"
|
||||
assert response.models.available_models[0].model_id == "devstral-latest"
|
||||
assert response.models.available_models[0].name == "devstral-latest"
|
||||
assert response.models.available_models[1].model_id == "devstral-small"
|
||||
assert response.models.available_models[1].name == "devstral-small"
|
||||
|
||||
assert response.modes is not None
|
||||
assert response.modes.current_mode_id == BuiltinAgentName.DEFAULT
|
||||
modes_ids = {m.id for m in response.modes.available_modes}
|
||||
assert modes_ids == {
|
||||
BuiltinAgentName.DEFAULT,
|
||||
BuiltinAgentName.CHAT,
|
||||
BuiltinAgentName.AUTO_APPROVE,
|
||||
BuiltinAgentName.PLAN,
|
||||
BuiltinAgentName.ACCEPT_EDITS,
|
||||
}
|
||||
|
||||
assert response.config_options is not None
|
||||
assert len(response.config_options) == 2
|
||||
assert response.config_options[0].root.id == "mode"
|
||||
assert response.config_options[0].root.category == "mode"
|
||||
assert response.config_options[0].root.current_value == BuiltinAgentName.DEFAULT
|
||||
assert len(response.config_options[0].root.options) == 5
|
||||
mode_option_values = {
|
||||
opt.value for opt in response.config_options[0].root.options
|
||||
}
|
||||
assert mode_option_values == {
|
||||
BuiltinAgentName.DEFAULT,
|
||||
BuiltinAgentName.CHAT,
|
||||
BuiltinAgentName.AUTO_APPROVE,
|
||||
BuiltinAgentName.PLAN,
|
||||
BuiltinAgentName.ACCEPT_EDITS,
|
||||
}
|
||||
assert response.config_options[1].root.id == "model"
|
||||
assert response.config_options[1].root.category == "model"
|
||||
assert response.config_options[1].root.current_value == "devstral-latest"
|
||||
assert len(response.config_options[1].root.options) == 2
|
||||
model_option_values = {
|
||||
opt.value for opt in response.config_options[1].root.options
|
||||
}
|
||||
assert model_option_values == {"devstral-latest", "devstral-small"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_registers_session_with_original_id(
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class TestACPNewSession:
|
|||
)
|
||||
|
||||
new_session_events = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe/new_session"
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe.new_session"
|
||||
]
|
||||
assert len(new_session_events) == 1
|
||||
assert new_session_events[0]["properties"]["entrypoint"] == "acp"
|
||||
|
|
@ -84,18 +84,47 @@ class TestACPNewSession:
|
|||
assert session_response.modes is not None
|
||||
assert session_response.modes.current_mode_id is not None
|
||||
assert session_response.modes.available_modes is not None
|
||||
assert len(session_response.modes.available_modes) == 4
|
||||
assert len(session_response.modes.available_modes) == 5
|
||||
|
||||
assert session_response.modes.current_mode_id == BuiltinAgentName.DEFAULT
|
||||
# Check that all primary agents are available (order may vary)
|
||||
mode_ids = {m.id for m in session_response.modes.available_modes}
|
||||
assert mode_ids == {
|
||||
BuiltinAgentName.DEFAULT,
|
||||
BuiltinAgentName.CHAT,
|
||||
BuiltinAgentName.AUTO_APPROVE,
|
||||
BuiltinAgentName.PLAN,
|
||||
BuiltinAgentName.ACCEPT_EDITS,
|
||||
}
|
||||
|
||||
# Check config_options
|
||||
assert session_response.config_options is not None
|
||||
assert len(session_response.config_options) == 2
|
||||
|
||||
# Mode config option
|
||||
mode_config = session_response.config_options[0]
|
||||
assert mode_config.root.id == "mode"
|
||||
assert mode_config.root.category == "mode"
|
||||
assert mode_config.root.current_value == BuiltinAgentName.DEFAULT
|
||||
assert len(mode_config.root.options) == 5
|
||||
mode_option_values = {opt.value for opt in mode_config.root.options}
|
||||
assert mode_option_values == {
|
||||
BuiltinAgentName.DEFAULT,
|
||||
BuiltinAgentName.CHAT,
|
||||
BuiltinAgentName.AUTO_APPROVE,
|
||||
BuiltinAgentName.PLAN,
|
||||
BuiltinAgentName.ACCEPT_EDITS,
|
||||
}
|
||||
|
||||
# Model config option
|
||||
model_config = session_response.config_options[1]
|
||||
assert model_config.root.id == "model"
|
||||
assert model_config.root.category == "model"
|
||||
assert model_config.root.current_value == "devstral-latest"
|
||||
assert len(model_config.root.options) == 2
|
||||
model_option_values = {opt.value for opt in model_config.root.options}
|
||||
assert model_option_values == {"devstral-latest", "devstral-small"}
|
||||
|
||||
@pytest.mark.skip(reason="TODO: Fix this test")
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_session_preserves_model_after_set_model(
|
||||
|
|
|
|||
287
tests/acp/test_set_config_option.py
Normal file
287
tests/acp/test_set_config_option.py
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.acp.conftest import _create_acp_agent
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config import ModelConfig, VibeConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_loop(backend) -> VibeAcpAgentLoop:
|
||||
config = build_test_vibe_config(
|
||||
active_model="devstral-latest",
|
||||
models=[
|
||||
ModelConfig(
|
||||
name="devstral-latest",
|
||||
provider="mistral",
|
||||
alias="devstral-latest",
|
||||
input_price=0.4,
|
||||
output_price=2.0,
|
||||
),
|
||||
ModelConfig(
|
||||
name="devstral-small",
|
||||
provider="mistral",
|
||||
alias="devstral-small",
|
||||
input_price=0.1,
|
||||
output_price=0.3,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
VibeConfig.dump_config(config.model_dump())
|
||||
|
||||
class PatchedAgentLoop(AgentLoop):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **{**kwargs, "backend": backend})
|
||||
self._base_config = config
|
||||
self.agent_manager.invalidate_config()
|
||||
try:
|
||||
active_model = config.get_active_model()
|
||||
self.stats.input_price_per_million = active_model.input_price
|
||||
self.stats.output_price_per_million = active_model.output_price
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgentLoop).start()
|
||||
|
||||
return _create_acp_agent()
|
||||
|
||||
|
||||
class TestACPSetConfigOptionMode:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_mode_to_auto_approve(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.DEFAULT
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="mode", value=BuiltinAgentName.AUTO_APPROVE
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.config_options is not None
|
||||
assert len(response.config_options) == 2
|
||||
assert (
|
||||
acp_session.agent_loop.agent_profile.name == BuiltinAgentName.AUTO_APPROVE
|
||||
)
|
||||
assert acp_session.agent_loop.auto_approve is True
|
||||
|
||||
# Verify config_options reflect the new state
|
||||
mode_config = response.config_options[0]
|
||||
assert mode_config.root.id == "mode"
|
||||
assert mode_config.root.current_value == BuiltinAgentName.AUTO_APPROVE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_mode_to_plan(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="mode", value=BuiltinAgentName.PLAN
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.PLAN
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_mode_invalid_returns_none(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
initial_mode = acp_session.agent_loop.agent_profile.name
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="mode", value="invalid-mode"
|
||||
)
|
||||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop.agent_profile.name == initial_mode
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_mode_empty_string_returns_none(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
initial_mode = acp_session.agent_loop.agent_profile.name
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="mode", value=""
|
||||
)
|
||||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop.agent_profile.name == initial_mode
|
||||
|
||||
|
||||
class TestACPSetConfigOptionModel:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_model_success(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
assert acp_session.agent_loop.config.active_model == "devstral-latest"
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="model", value="devstral-small"
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.config_options is not None
|
||||
assert len(response.config_options) == 2
|
||||
assert acp_session.agent_loop.config.active_model == "devstral-small"
|
||||
|
||||
# Verify config_options reflect the new state
|
||||
model_config = response.config_options[1]
|
||||
assert model_config.root.id == "model"
|
||||
assert model_config.root.current_value == "devstral-small"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_model_invalid_returns_none(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
initial_model = acp_session.agent_loop.config.active_model
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="model", value="non-existent-model"
|
||||
)
|
||||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop.config.active_model == initial_model
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_model_empty_string_returns_none(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
initial_model = acp_session.agent_loop.config.active_model
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="model", value=""
|
||||
)
|
||||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop.config.active_model == initial_model
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_model_saves_to_config(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.save_updates") as mock_save:
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="model", value="devstral-small"
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
mock_save.assert_called_once_with({"active_model": "devstral-small"})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_model_does_not_save_on_invalid(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.save_updates") as mock_save:
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="model", value="non-existent-model"
|
||||
)
|
||||
|
||||
assert response is None
|
||||
mock_save.assert_not_called()
|
||||
|
||||
|
||||
class TestACPSetConfigOptionInvalidConfigId:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_invalid_config_id_returns_none(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="invalid_config", value="some_value"
|
||||
)
|
||||
|
||||
assert response is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_empty_config_id_returns_none(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="", value="some_value"
|
||||
)
|
||||
|
||||
assert response is None
|
||||
|
|
@ -106,6 +106,29 @@ class TestACPSetMode:
|
|||
acp_session.agent_loop.auto_approve is False
|
||||
) # Accept Edits mode doesn't auto-approve all
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_to_chat(self, acp_agent_loop: VibeAcpAgentLoop) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.DEFAULT
|
||||
|
||||
response = await acp_agent_loop.set_session_mode(
|
||||
session_id=session_id, mode_id=BuiltinAgentName.CHAT
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.CHAT
|
||||
assert (
|
||||
acp_session.agent_loop.auto_approve is True
|
||||
) # Chat mode auto-approves read-only tools
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_invalid_mode_returns_none(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
|
|
|
|||
44
tests/acp/test_tool_call_session_update.py
Normal file
44
tests/acp/test_tool_call_session_update.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from acp.schema import ToolCallStart
|
||||
|
||||
from vibe.acp.tools.session_update import tool_call_session_update
|
||||
from vibe.core.tools.builtins.read_file import ReadFile, ReadFileArgs
|
||||
from vibe.core.types import ToolCallEvent
|
||||
|
||||
|
||||
class TestToolCallSessionUpdate:
|
||||
def _create_event(self) -> ToolCallEvent:
|
||||
return ToolCallEvent(
|
||||
tool_name="read_file",
|
||||
tool_call_id="test_call_123",
|
||||
args=ReadFileArgs(path="/tmp/test.txt"),
|
||||
tool_class=ReadFile,
|
||||
)
|
||||
|
||||
def test_returns_tool_call_start(self) -> None:
|
||||
event = self._create_event()
|
||||
|
||||
update = tool_call_session_update(event)
|
||||
|
||||
assert update is not None
|
||||
assert isinstance(update, ToolCallStart)
|
||||
assert update.session_update == "tool_call"
|
||||
assert update.tool_call_id == "test_call_123"
|
||||
|
||||
def test_returns_tool_call_start_for_streaming_event(self) -> None:
|
||||
event = ToolCallEvent(
|
||||
tool_name="read_file",
|
||||
tool_call_id="test_call_123",
|
||||
tool_class=ReadFile,
|
||||
args=None,
|
||||
)
|
||||
|
||||
update = tool_call_session_update(event)
|
||||
|
||||
assert update is not None
|
||||
assert isinstance(update, ToolCallStart)
|
||||
assert update.session_update == "tool_call"
|
||||
assert update.tool_call_id == "test_call_123"
|
||||
assert update.kind == "read"
|
||||
assert update.raw_input is None
|
||||
Loading…
Add table
Add a link
Reference in a new issue