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>
This commit is contained in:
Clément Drouin 2026-06-16 10:43:46 +02:00 committed by GitHub
parent c2cb612ac1
commit 564a14365e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 952 additions and 252 deletions

View file

@ -72,7 +72,7 @@ class TestACPInitialize:
),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.16.0"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.16.1"
)
assert response.auth_methods is not None
@ -172,7 +172,7 @@ class TestACPInitialize:
),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.16.0"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.16.1"
)
assert response.auth_methods is not None

View file

@ -1,11 +1,13 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock
from acp import RequestError
from acp.schema import (
AgentMessageChunk,
AgentThoughtChunk,
ClientCapabilities,
ToolCallProgress,
ToolCallStart,
UserMessageChunk,
@ -15,10 +17,11 @@ import pytest
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.acp.acp_agent_loop import WORKSPACE_TRUST_CAPABILITY, VibeAcpAgentLoop
from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.config import ModelConfig, SessionLoggingConfig
from vibe.core.trusted_folders import trusted_folders_manager
from vibe.core.types import Role
@ -122,6 +125,37 @@ class TestLoadSession:
assert response.config_options[2].category == "thinking"
assert response.config_options[2].current_value == "off"
@pytest.mark.asyncio
async def test_load_session_resolves_workspace_trust_before_loading_config(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
acp_agent, client = acp_agent_with_session_config
acp_agent.client_capabilities = ClientCapabilities(
field_meta={WORKSPACE_TRUST_CAPABILITY: True}
)
request_trust = AsyncMock(return_value={"decision": "trust_cwd"})
monkeypatch.setattr(client, "ext_method", request_trust)
(tmp_working_directory / "AGENTS.md").write_text(
"Loaded session project instructions", encoding="utf-8"
)
session_id = "test-sess-trust"
create_test_session(temp_session_dir, session_id, str(tmp_working_directory))
await acp_agent.load_session(
cwd=str(tmp_working_directory), mcp_servers=[], session_id=session_id
)
request_trust.assert_awaited_once()
assert trusted_folders_manager.is_trusted(tmp_working_directory) is True
system_prompt = acp_agent.sessions[session_id].agent_loop.messages[0].content
assert system_prompt is not None
assert "Loaded session project instructions" in system_prompt
@pytest.mark.asyncio
async def test_load_session_registers_session_with_original_id(
self,

View file

@ -1,16 +1,31 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
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 VibeAcpAgentLoop
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
@ -137,6 +152,231 @@ class TestACPNewSession:
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(

View file

@ -194,7 +194,9 @@ def test_failed_update_prints_error_message(
):
_maybe_run_startup_update_prompt(config, repository)
assert "could not be updated automatically" in capsys.readouterr().out
out = capsys.readouterr().out
assert "could not update automatically" in out
assert "package manager" in out
def test_failed_update_does_not_dismiss_so_user_is_reprompted_on_next_launch(

View file

@ -1,18 +1,82 @@
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import asyncio
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime
from unittest.mock import MagicMock
import pytest
from vibe.core.nuage.workflow import (
WorkflowExecutionListResponse,
WorkflowExecutionStatus,
WorkflowExecutionWithoutResultResponse,
)
from vibe.core.session.resume_sessions import (
RemoteResumeResult,
RemoteResumeSessions,
ResumeSessionInfo,
can_delete_resume_session_source,
list_remote_resume_sessions,
session_latest_messages,
short_session_id,
)
from vibe.core.session.session_id import shorten_session_id
@dataclass(frozen=True)
class RemoteResumeRequest:
workflow_identifier: str | None
page_size: int
status: Sequence[WorkflowExecutionStatus] | None
class FakeRemoteResumeClient:
def __init__(
self,
response: WorkflowExecutionListResponse | None = None,
*,
delay: float = 0.0,
) -> None:
self.response = response or WorkflowExecutionListResponse(executions=[])
self.delay = delay
self.requests: list[RemoteResumeRequest] = []
self.closed = False
async def get_workflow_runs(
self,
workflow_identifier: str | None = None,
page_size: int = 50,
next_page_token: str | None = None,
status: Sequence[WorkflowExecutionStatus] | None = None,
user_id: str = "current",
) -> WorkflowExecutionListResponse:
self.requests.append(
RemoteResumeRequest(
workflow_identifier=workflow_identifier,
page_size=page_size,
status=status,
)
)
if self.delay:
await asyncio.sleep(self.delay)
return self.response
async def aclose(self) -> None:
self.closed = True
def enabled_vibe_code_config() -> MagicMock:
config = MagicMock()
config.vibe_code_enabled = True
config.vibe_code_api_key = "test-key"
config.vibe_code_base_url = "https://test.example.com"
config.api_timeout = 30
config.vibe_code_workflow_id = "workflow-1"
return config
class TestShortenSessionId:
def test_shortens_to_first_8_chars(self) -> None:
sid = "abcdef1234567890"
@ -67,40 +131,8 @@ class TestCanDeleteResumeSession:
class TestListRemoteResumeSessions:
@pytest.mark.asyncio
async def test_returns_empty_when_vibe_code_disabled(self) -> None:
config = MagicMock()
config.vibe_code_enabled = False
config.vibe_code_api_key = "key"
result = await list_remote_resume_sessions(config)
assert result == []
@pytest.mark.asyncio
async def test_returns_empty_when_no_api_key(self) -> None:
config = MagicMock()
config.vibe_code_enabled = True
config.vibe_code_api_key = None
result = await list_remote_resume_sessions(config)
assert result == []
@pytest.mark.asyncio
async def test_returns_empty_when_both_missing(self) -> None:
config = MagicMock()
config.vibe_code_enabled = False
config.vibe_code_api_key = None
result = await list_remote_resume_sessions(config)
assert result == []
@pytest.mark.asyncio
async def test_passes_active_statuses_to_api(self) -> None:
from datetime import datetime
from vibe.core.nuage.workflow import (
WorkflowExecutionListResponse,
WorkflowExecutionStatus,
WorkflowExecutionWithoutResultResponse,
)
running = WorkflowExecutionWithoutResultResponse(
workflow_name="vibe",
execution_id="exec-running",
@ -117,47 +149,28 @@ class TestListRemoteResumeSessions:
)
mock_response = WorkflowExecutionListResponse(executions=[running, continued])
client = FakeRemoteResumeClient(mock_response)
config = MagicMock()
config.vibe_code_enabled = True
config.vibe_code_api_key = "test-key"
config.vibe_code_base_url = "https://test.example.com"
config.api_timeout = 30
config.vibe_code_workflow_id = "workflow-1"
with patch("vibe.core.session.resume_sessions.WorkflowsClient") as MockClient:
mock_client = AsyncMock()
mock_client.get_workflow_runs.return_value = mock_response
MockClient.return_value.__aenter__ = AsyncMock(return_value=mock_client)
MockClient.return_value.__aexit__ = AsyncMock(return_value=False)
result = await list_remote_resume_sessions(config)
result = await list_remote_resume_sessions(client, "workflow-1")
assert len(result) == 2
session_ids = {s.session_id for s in result}
assert "exec-running" in session_ids
assert "exec-continued" in session_ids
assert all(s.source == "remote" for s in result)
mock_client.get_workflow_runs.assert_called_once_with(
workflow_identifier="workflow-1",
page_size=50,
status=[
WorkflowExecutionStatus.RUNNING,
WorkflowExecutionStatus.CONTINUED_AS_NEW,
],
)
assert client.requests == [
RemoteResumeRequest(
workflow_identifier="workflow-1",
page_size=50,
status=[
WorkflowExecutionStatus.RUNNING,
WorkflowExecutionStatus.CONTINUED_AS_NEW,
],
)
]
@pytest.mark.asyncio
async def test_deduplicates_execution_ids_keeps_latest(self) -> None:
from datetime import datetime
from vibe.core.nuage.workflow import (
WorkflowExecutionListResponse,
WorkflowExecutionStatus,
WorkflowExecutionWithoutResultResponse,
)
older = WorkflowExecutionWithoutResultResponse(
workflow_name="vibe",
execution_id="exec-1",
@ -181,21 +194,9 @@ class TestListRemoteResumeSessions:
)
mock_response = WorkflowExecutionListResponse(executions=[older, newer, other])
client = FakeRemoteResumeClient(mock_response)
config = MagicMock()
config.vibe_code_enabled = True
config.vibe_code_api_key = "test-key"
config.vibe_code_base_url = "https://test.example.com"
config.api_timeout = 30
config.vibe_code_workflow_id = "workflow-1"
with patch("vibe.core.session.resume_sessions.WorkflowsClient") as MockClient:
mock_client = AsyncMock()
mock_client.get_workflow_runs.return_value = mock_response
MockClient.return_value.__aenter__ = AsyncMock(return_value=mock_client)
MockClient.return_value.__aexit__ = AsyncMock(return_value=False)
result = await list_remote_resume_sessions(config)
result = await list_remote_resume_sessions(client, "workflow-1")
assert len(result) == 2
by_id = {s.session_id: s for s in result}
@ -206,14 +207,6 @@ class TestListRemoteResumeSessions:
async def test_dedup_keeps_latest_start_time_when_previous_has_end_time(
self,
) -> None:
from datetime import datetime
from vibe.core.nuage.workflow import (
WorkflowExecutionListResponse,
WorkflowExecutionStatus,
WorkflowExecutionWithoutResultResponse,
)
previous = WorkflowExecutionWithoutResultResponse(
workflow_name="vibe",
execution_id="exec-1",
@ -230,22 +223,85 @@ class TestListRemoteResumeSessions:
)
mock_response = WorkflowExecutionListResponse(executions=[previous, newer])
client = FakeRemoteResumeClient(mock_response)
config = MagicMock()
config.vibe_code_enabled = True
config.vibe_code_api_key = "test-key"
config.vibe_code_base_url = "https://test.example.com"
config.api_timeout = 30
config.vibe_code_workflow_id = "workflow-1"
with patch("vibe.core.session.resume_sessions.WorkflowsClient") as MockClient:
mock_client = AsyncMock()
mock_client.get_workflow_runs.return_value = mock_response
MockClient.return_value.__aenter__ = AsyncMock(return_value=mock_client)
MockClient.return_value.__aexit__ = AsyncMock(return_value=False)
result = await list_remote_resume_sessions(config)
result = await list_remote_resume_sessions(client, "workflow-1")
assert len(result) == 1
assert result[0].session_id == "exec-1"
assert result[0].status == WorkflowExecutionStatus.RUNNING
class TestSessionLatestMessages:
def test_remote_session_formats_title_and_status(self) -> None:
session = ResumeSessionInfo(
session_id="exec-1",
source="remote",
cwd="",
title="My run",
end_time=None,
status="RUNNING",
)
messages = session_latest_messages([session], MagicMock())
assert messages[session.option_id] == "My run (running)"
class TestRemoteResumeSessions:
@pytest.mark.asyncio
async def test_fetch_skips_when_vibe_code_disabled(self) -> None:
config = MagicMock()
config.vibe_code_enabled = False
config.vibe_code_api_key = "key"
created_clients: list[FakeRemoteResumeClient] = []
def client_factory(_config: object) -> FakeRemoteResumeClient:
client = FakeRemoteResumeClient()
created_clients.append(client)
return client
remote = RemoteResumeSessions(lambda: config, client_factory)
result = await remote.fetch(10.0)
assert result == RemoteResumeResult([], None)
assert created_clients == []
@pytest.mark.asyncio
async def test_start_cancels_previous_fetch(self) -> None:
config = enabled_vibe_code_config()
client = FakeRemoteResumeClient(delay=10.0)
remote = RemoteResumeSessions(lambda: config, lambda _config: client)
first = remote.start(100.0)
await asyncio.sleep(0)
second = remote.start(100.0)
await asyncio.sleep(0)
await remote.aclose()
assert first.cancelled()
assert first is not second
@pytest.mark.asyncio
async def test_fetch_returns_error_tuple_on_timeout(self) -> None:
config = enabled_vibe_code_config()
client = FakeRemoteResumeClient(delay=10.0)
remote = RemoteResumeSessions(lambda: config, lambda _config: client)
sessions, error = await remote.fetch(0.01)
await remote.aclose()
assert sessions == []
assert error is not None and "Timed out" in error
@pytest.mark.asyncio
async def test_aclose_cancels_inflight_fetch_before_closing_client(self) -> None:
config = enabled_vibe_code_config()
client = FakeRemoteResumeClient(delay=10.0)
remote = RemoteResumeSessions(lambda: config, lambda _config: client)
task = remote.start(100.0)
await asyncio.sleep(0)
await remote.aclose()
assert task.cancelled()
assert client.closed is True