Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-03-23 21:59:24 +01:00 committed by GitHub
parent 7b8a66bc6c
commit 5ea69b547b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 88 additions and 378 deletions

View file

@ -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.6.1"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.6.2"
)
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.6.1"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.6.2"
)
assert response.auth_methods is not None

View file

@ -100,11 +100,13 @@ class TestLoadSession:
assert response.config_options is not None
assert len(response.config_options) == 2
assert response.config_options[0].id == "mode"
assert response.config_options[0].category == "mode"
assert response.config_options[0].current_value == BuiltinAgentName.DEFAULT
assert len(response.config_options[0].options) == 5
mode_option_values = {opt.value for opt in response.config_options[0].options}
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,
@ -112,11 +114,13 @@ class TestLoadSession:
BuiltinAgentName.PLAN,
BuiltinAgentName.ACCEPT_EDITS,
}
assert response.config_options[1].id == "model"
assert response.config_options[1].category == "model"
assert response.config_options[1].current_value == "devstral-latest"
assert len(response.config_options[1].options) == 2
model_option_values = {opt.value for opt in response.config_options[1].options}
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

View file

@ -103,11 +103,11 @@ class TestACPNewSession:
# Mode config option
mode_config = session_response.config_options[0]
assert mode_config.id == "mode"
assert mode_config.category == "mode"
assert mode_config.current_value == BuiltinAgentName.DEFAULT
assert len(mode_config.options) == 5
mode_option_values = {opt.value for opt in mode_config.options}
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,
@ -118,11 +118,11 @@ class TestACPNewSession:
# Model config option
model_config = session_response.config_options[1]
assert model_config.id == "model"
assert model_config.category == "model"
assert model_config.current_value == "devstral-latest"
assert len(model_config.options) == 2
model_option_values = {opt.value for opt in model_config.options}
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")

View file

@ -83,8 +83,8 @@ class TestACPSetConfigOptionMode:
# Verify config_options reflect the new state
mode_config = response.config_options[0]
assert mode_config.id == "mode"
assert mode_config.current_value == BuiltinAgentName.AUTO_APPROVE
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(
@ -133,8 +133,8 @@ class TestACPSetConfigOptionMode:
) # Chat mode auto-approves read-only tools
mode_config = response.config_options[0]
assert mode_config.id == "mode"
assert mode_config.current_value == BuiltinAgentName.CHAT
assert mode_config.root.id == "mode"
assert mode_config.root.current_value == BuiltinAgentName.CHAT
@pytest.mark.asyncio
async def test_set_config_option_mode_invalid_returns_none(
@ -205,8 +205,8 @@ class TestACPSetConfigOptionModel:
# Verify config_options reflect the new state
model_config = response.config_options[1]
assert model_config.id == "model"
assert model_config.current_value == "devstral-small"
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(

View file

@ -1,254 +0,0 @@
from __future__ import annotations
import asyncio
import json
from pathlib import Path
from unittest.mock import patch
from acp.schema import TextContentBlock, UsageUpdate
import pytest
from tests.acp.conftest import _create_acp_agent
from tests.conftest import build_test_vibe_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.core.agent_loop import AgentLoop
from vibe.core.config import SessionLoggingConfig
from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
def _make_backend(prompt_tokens: int = 100, completion_tokens: int = 50) -> FakeBackend:
return FakeBackend(
LLMChunk(
message=LLMMessage(role=Role.assistant, content="Hi"),
usage=LLMUsage(
prompt_tokens=prompt_tokens, completion_tokens=completion_tokens
),
)
)
def _make_acp_agent(backend: FakeBackend) -> VibeAcpAgentLoop:
config = build_test_vibe_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()
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgentLoop).start()
return _create_acp_agent()
def _get_fake_client(agent: VibeAcpAgentLoop) -> FakeClient:
return agent.client # type: ignore[return-value]
def _get_usage_updates(client: FakeClient) -> list[UsageUpdate]:
return [
update.update
for update in client._session_updates
if isinstance(update.update, UsageUpdate)
]
class TestPromptResponseUsage:
@pytest.mark.asyncio
async def test_prompt_returns_usage_in_response(self) -> None:
agent = _make_acp_agent(_make_backend(prompt_tokens=100, completion_tokens=50))
session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
response = await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello")],
)
assert response.usage is not None
assert response.usage.input_tokens == 100
assert response.usage.output_tokens == 50
assert response.usage.total_tokens == 150
@pytest.mark.asyncio
async def test_prompt_usage_optional_fields_are_none(self) -> None:
agent = _make_acp_agent(_make_backend())
session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
response = await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello")],
)
assert response.usage is not None
assert response.usage.thought_tokens is None
assert response.usage.cached_read_tokens is None
assert response.usage.cached_write_tokens is None
@pytest.mark.asyncio
async def test_prompt_usage_accumulates_across_turns(self) -> None:
backend = _make_backend(prompt_tokens=100, completion_tokens=50)
agent = _make_acp_agent(backend)
session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
first = await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello")],
)
second = await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello again")],
)
assert first.usage is not None
assert second.usage is not None
# Second turn should have strictly more cumulative tokens
assert second.usage.input_tokens > first.usage.input_tokens
assert second.usage.output_tokens > first.usage.output_tokens
assert second.usage.total_tokens > first.usage.total_tokens
class TestUsageUpdateNotification:
@pytest.mark.asyncio
async def test_prompt_sends_usage_update(self) -> None:
agent = _make_acp_agent(_make_backend())
session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello")],
)
await asyncio.sleep(0)
usage_updates = _get_usage_updates(_get_fake_client(agent))
assert len(usage_updates) == 1
assert usage_updates[0].session_update == "usage_update"
@pytest.mark.asyncio
async def test_usage_update_contains_context_window_info(self) -> None:
agent = _make_acp_agent(_make_backend(prompt_tokens=100, completion_tokens=50))
session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello")],
)
await asyncio.sleep(0)
usage_updates = _get_usage_updates(_get_fake_client(agent))
assert len(usage_updates) == 1
assert usage_updates[0].size > 0
assert usage_updates[0].used > 0
@pytest.mark.asyncio
async def test_usage_update_contains_cost_when_pricing_set(self) -> None:
agent = _make_acp_agent(
_make_backend(prompt_tokens=1_000_000, completion_tokens=500_000)
)
session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
# Set pricing directly on the session stats (config loading uses fixture defaults)
acp_session = agent.sessions[session.session_id]
acp_session.agent_loop.stats.update_pricing(input_price=0.4, output_price=2.0)
await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello")],
)
await asyncio.sleep(0)
usage_updates = _get_usage_updates(_get_fake_client(agent))
assert len(usage_updates) == 1
cost = usage_updates[0].cost
assert cost is not None
assert cost.currency == "USD"
assert cost.amount > 0
@pytest.mark.asyncio
async def test_usage_update_no_cost_when_zero_pricing(self) -> None:
agent = _make_acp_agent(_make_backend())
session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello")],
)
await asyncio.sleep(0)
usage_updates = _get_usage_updates(_get_fake_client(agent))
assert len(usage_updates) == 1
assert usage_updates[0].cost is None
@pytest.mark.asyncio
async def test_usage_update_sent_per_prompt(self) -> None:
backend = _make_backend()
agent = _make_acp_agent(backend)
session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello")],
)
await asyncio.sleep(0)
await agent.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text="Hello again")],
)
await asyncio.sleep(0)
usage_updates = _get_usage_updates(_get_fake_client(agent))
assert len(usage_updates) == 2
class TestLoadSessionUsageUpdate:
def _make_session_dir(self, tmp_path: Path, session_id: str, cwd: str) -> Path:
session_folder = tmp_path / f"session_20240101_120000_{session_id[:8]}"
session_folder.mkdir()
messages_file = session_folder / "messages.jsonl"
with messages_file.open("w") as f:
f.write(json.dumps({"role": "user", "content": "Hello"}) + "\n")
meta = {
"session_id": session_id,
"start_time": "2024-01-01T12:00:00Z",
"end_time": "2024-01-01T12:05:00Z",
"environment": {"working_directory": cwd},
}
with (session_folder / "meta.json").open("w") as f:
json.dump(meta, f)
return session_folder
def _make_agent_with_session_logging(
self, backend: FakeBackend, session_dir: Path
) -> VibeAcpAgentLoop:
session_config = SessionLoggingConfig(
save_dir=str(session_dir), session_prefix="session", enabled=True
)
config = build_test_vibe_config(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()
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgentLoop).start()
agent = _create_acp_agent()
patch.object(agent, "_load_config", return_value=config).start()
return agent
@pytest.mark.asyncio
async def test_load_session_sends_usage_update(self, tmp_path: Path) -> None:
backend = _make_backend()
agent = self._make_agent_with_session_logging(backend, tmp_path)
session_id = "test-session-load-usage"
self._make_session_dir(tmp_path, session_id, str(Path.cwd()))
await agent.load_session(cwd=str(Path.cwd()), session_id=session_id)
await asyncio.sleep(0)
client = _get_fake_client(agent)
usage_updates = _get_usage_updates(client)
assert len(usage_updates) == 1
assert usage_updates[0].session_update == "usage_update"
assert usage_updates[0].size > 0