vibe/tests/acp/test_new_session.py
Clément Drouin 564a14365e
v2.16.1 (#808)
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-06-16 10:43:46 +02:00

403 lines
15 KiB
Python

from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock, patch
from acp import RequestError
from acp.schema import ClientCapabilities
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 WORKSPACE_TRUST_CAPABILITY, VibeAcpAgentLoop
from vibe.acp.exceptions import InvalidRequestError
from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.config import ModelConfig
from vibe.core.trusted_folders import trusted_folders_manager
def _system_prompt(acp_agent_loop: VibeAcpAgentLoop, session_id: str) -> str:
session = acp_agent_loop.sessions[session_id]
return session.agent_loop.messages[0].content or ""
def _enable_workspace_trust(acp_agent_loop: VibeAcpAgentLoop) -> None:
acp_agent_loop.client_capabilities = ClientCapabilities(
field_meta={WORKSPACE_TRUST_CAPABILITY: True}
)
@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"
),
ModelConfig(
name="devstral-small", provider="mistral", alias="devstral-small"
),
],
)
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()
class TestACPNewSession:
@pytest.mark.asyncio
async def test_new_session_response_structure(
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict]
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
assert session_response.session_id is not None
acp_session = next(
(
s
for s in acp_agent_loop.sessions.values()
if s.id == session_response.session_id
),
None,
)
assert acp_session is not None
# Telemetry now fires from the background warm-up worker once
# `wait_until_ready` joins both MCP and experiments. Awaiting it here
# forces emission before assertions.
await acp_session.agent_loop.wait_until_ready()
new_session_events = [
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"
assert (
acp_session.agent_loop.session_logger.session_id
== session_response.session_id
)
assert session_response.session_id == acp_session.agent_loop.session_id
assert session_response.models is not None
assert session_response.models.current_model_id is not None
assert session_response.models.available_models is not None
assert len(session_response.models.available_models) == 2
assert session_response.models.current_model_id == "devstral-latest"
assert session_response.models.available_models[0].model_id == "devstral-latest"
assert session_response.models.available_models[0].name == "devstral-latest"
assert session_response.models.available_models[1].model_id == "devstral-small"
assert session_response.models.available_models[1].name == "devstral-small"
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) == 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) == 3
# 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_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.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_option_values == {"devstral-latest", "devstral-small"}
# Thinking config option
thinking_config = session_response.config_options[2]
assert thinking_config.id == "thinking"
assert thinking_config.category == "thinking"
assert thinking_config.current_value == "off"
assert len(thinking_config.options) == 5
@pytest.mark.asyncio
async def test_new_session_loads_root_agents_md_from_workspace_cwd(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Root project instructions", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
request_trust = AsyncMock(return_value={"decision": "trust_cwd"})
monkeypatch.setattr(acp_agent_loop.client, "ext_method", request_trust)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
request_trust.assert_awaited_once()
await_args = request_trust.await_args
assert await_args is not None
method, params = await_args.args
assert method == "trust/request"
assert params["cwd"] == str(tmp_working_directory.resolve())
assert params["detectedFiles"] == ["AGENTS.md"]
assert params["repoDetectedFiles"] == []
assert params["availableDecisions"] == ["trust_cwd", "trust_session", "decline"]
assert trusted_folders_manager.is_trusted(tmp_working_directory) is True
assert "Root project instructions" in _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_can_trust_full_repo_from_subdirectory(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo = tmp_path / "repo"
cwd = repo / "src" / "pkg"
(repo / ".git").mkdir(parents=True)
(repo / ".git" / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
cwd.mkdir(parents=True)
(repo / "AGENTS.md").write_text("Repo instructions", encoding="utf-8")
_enable_workspace_trust(acp_agent_loop)
request_trust = AsyncMock(return_value={"decision": "trust_repo"})
monkeypatch.setattr(acp_agent_loop.client, "ext_method", request_trust)
session_response = await acp_agent_loop.new_session(
cwd=str(cwd), mcp_servers=[]
)
assert session_response.session_id is not None
request_trust.assert_awaited_once()
await_args = request_trust.await_args
assert await_args is not None
method, params = await_args.args
assert method == "trust/request"
assert params["cwd"] == str(cwd.resolve())
assert params["repoRoot"] == str(repo.resolve())
assert params["detectedFiles"] == []
assert params["repoDetectedFiles"] == ["AGENTS.md"]
assert params["availableDecisions"] == [
"trust_repo",
"trust_cwd",
"trust_session",
"decline",
]
assert trusted_folders_manager.is_trusted(repo) is True
assert trusted_folders_manager.is_trusted(cwd) is True
assert "Repo instructions" in _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_decline_skips_project_docs(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Do not load this", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
monkeypatch.setattr(
acp_agent_loop.client,
"ext_method",
AsyncMock(return_value={"decision": "decline"}),
)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
assert trusted_folders_manager.is_trusted(tmp_working_directory) is False
assert "Do not load this" not in _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_session_trust_loads_docs_without_persisting(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Session-only instructions", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
monkeypatch.setattr(
acp_agent_loop.client,
"ext_method",
AsyncMock(return_value={"decision": "trust_session"}),
)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
normalized = str(tmp_working_directory.resolve())
assert trusted_folders_manager.is_trusted(tmp_working_directory) is True
assert normalized in trusted_folders_manager._session_trusted
assert normalized not in trusted_folders_manager._trusted
assert "Session-only instructions" in _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_skips_trust_prompt_without_trustable_files(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_enable_workspace_trust(acp_agent_loop)
request_trust = AsyncMock(return_value={"decision": "trust_cwd"})
monkeypatch.setattr(acp_agent_loop.client, "ext_method", request_trust)
await acp_agent_loop.new_session(cwd=str(tmp_working_directory), mcp_servers=[])
request_trust.assert_not_awaited()
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
@pytest.mark.asyncio
async def test_new_session_direct_client_fallback_skips_project_docs(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Direct client should skip this", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
monkeypatch.setattr(
acp_agent_loop.client,
"ext_method",
AsyncMock(side_effect=RequestError.method_not_found("trust/request")),
)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
assert "Direct client should skip this" not in _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_without_workspace_trust_capability_skips_prompt(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Unsupported client should skip this", encoding="utf-8"
)
request_trust = AsyncMock(return_value={"decision": "trust_cwd"})
monkeypatch.setattr(acp_agent_loop.client, "ext_method", request_trust)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
request_trust.assert_not_awaited()
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
assert "Unsupported client should skip this" not in _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_cancelled_trust_prompt_cancels_session_creation(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Cancelled prompt", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
monkeypatch.setattr(
acp_agent_loop.client,
"ext_method",
AsyncMock(return_value={"decision": "cancelled"}),
)
with pytest.raises(InvalidRequestError):
await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
assert acp_agent_loop.sessions == {}
@pytest.mark.skip(reason="TODO: Fix this test")
@pytest.mark.asyncio
async def test_new_session_preserves_model_after_set_model(
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
assert session_response.models is not None
assert session_response.models.current_model_id == "devstral-latest"
response = await acp_agent_loop.set_session_model(
session_id=session_id, model_id="devstral-small"
)
assert response is not None
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
assert session_response.models is not None
assert session_response.models.current_model_id == "devstral-small"