Co-authored-by: Jean Burellier <sheplu@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: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: renovate-mistral[bot] <253709520+renovate-mistral[bot]@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
210 lines
7.2 KiB
Python
210 lines
7.2 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
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 vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
|
from vibe.acp.exceptions import InvalidRequestError, SessionNotFoundError
|
|
from vibe.core.agent_loop import AgentLoop
|
|
from vibe.core.config import ModelConfig
|
|
from vibe.core.trusted_folders import trusted_folders_manager
|
|
|
|
|
|
async def _system_prompt(acp_agent_loop: VibeAcpAgentLoop, session_id: str) -> str:
|
|
session = acp_agent_loop.sessions[session_id]
|
|
await session.agent_loop.wait_until_ready()
|
|
return session.agent_loop.messages[0].content or ""
|
|
|
|
|
|
async def _wait_for_background_tasks(
|
|
acp_agent_loop: VibeAcpAgentLoop, session_id: str
|
|
) -> None:
|
|
session = acp_agent_loop.sessions[session_id]
|
|
while session._tasks:
|
|
await asyncio.gather(*list(session._tasks))
|
|
|
|
|
|
@pytest.fixture
|
|
def acp_agent_loop(
|
|
backend: FakeBackend, monkeypatch: pytest.MonkeyPatch
|
|
) -> 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()
|
|
|
|
monkeypatch.setattr("vibe.acp.acp_agent_loop.AgentLoop", PatchedAgentLoop)
|
|
return _create_acp_agent()
|
|
|
|
|
|
class TestWorkspaceTrustExtMethods:
|
|
@pytest.mark.asyncio
|
|
async def test_workspace_trust_status_returns_details_even_after_decline(
|
|
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
|
|
) -> None:
|
|
(tmp_working_directory / "AGENTS.md").write_text(
|
|
"Trust me later", encoding="utf-8"
|
|
)
|
|
trusted_folders_manager.add_untrusted(tmp_working_directory)
|
|
|
|
response = await acp_agent_loop.ext_method(
|
|
"trust/status", {"cwd": str(tmp_working_directory)}
|
|
)
|
|
|
|
assert response == {
|
|
"trust_status": "untrusted",
|
|
"details": {
|
|
"cwd": str(tmp_working_directory.resolve()),
|
|
"repoRoot": None,
|
|
"ignoredFiles": ["AGENTS.md"],
|
|
"availableDecisions": ["trust_cwd", "decline"],
|
|
},
|
|
}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_workspace_trust_decision_trusts_cwd_and_reloads_session(
|
|
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
|
|
) -> None:
|
|
(tmp_working_directory / "AGENTS.md").write_text(
|
|
"Reloaded session instructions", encoding="utf-8"
|
|
)
|
|
|
|
session_response = await acp_agent_loop.new_session(
|
|
cwd=str(tmp_working_directory), mcp_servers=[]
|
|
)
|
|
assert session_response.session_id is not None
|
|
assert "Reloaded session instructions" not in await _system_prompt(
|
|
acp_agent_loop, session_response.session_id
|
|
)
|
|
|
|
response = await acp_agent_loop.ext_method(
|
|
"trust/decision",
|
|
{
|
|
"cwd": str(tmp_working_directory),
|
|
"decision": "trust_cwd",
|
|
"session_id": session_response.session_id,
|
|
},
|
|
)
|
|
|
|
assert response == {"trust_status": "trusted", "details": None}
|
|
normalized = str(tmp_working_directory.resolve())
|
|
assert normalized not in trusted_folders_manager._session_trusted
|
|
assert normalized in trusted_folders_manager._trusted
|
|
await _wait_for_background_tasks(acp_agent_loop, session_response.session_id)
|
|
assert "Reloaded session instructions" in await _system_prompt(
|
|
acp_agent_loop, session_response.session_id
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_workspace_trust_decision_returns_before_reload_completes(
|
|
self,
|
|
acp_agent_loop: VibeAcpAgentLoop,
|
|
tmp_working_directory: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
(tmp_working_directory / "AGENTS.md").write_text(
|
|
"Slow reload instructions", encoding="utf-8"
|
|
)
|
|
|
|
session_response = await acp_agent_loop.new_session(
|
|
cwd=str(tmp_working_directory), mcp_servers=[]
|
|
)
|
|
assert session_response.session_id is not None
|
|
|
|
reload_started = asyncio.Event()
|
|
release_reload = asyncio.Event()
|
|
|
|
async def slow_reload_session_config(session) -> None:
|
|
reload_started.set()
|
|
await release_reload.wait()
|
|
|
|
monkeypatch.setattr(
|
|
acp_agent_loop, "_reload_session_config", slow_reload_session_config
|
|
)
|
|
|
|
decision_task = asyncio.create_task(
|
|
acp_agent_loop.ext_method(
|
|
"trust/decision",
|
|
{
|
|
"cwd": str(tmp_working_directory),
|
|
"decision": "trust_cwd",
|
|
"session_id": session_response.session_id,
|
|
},
|
|
)
|
|
)
|
|
|
|
try:
|
|
await asyncio.wait_for(reload_started.wait(), timeout=1)
|
|
await asyncio.sleep(0)
|
|
|
|
assert decision_task.done()
|
|
assert decision_task.result() == {
|
|
"trust_status": "trusted",
|
|
"details": None,
|
|
}
|
|
finally:
|
|
release_reload.set()
|
|
await decision_task
|
|
await _wait_for_background_tasks(
|
|
acp_agent_loop, session_response.session_id
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_workspace_trust_decision_rejects_unavailable_decision(
|
|
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
|
|
) -> None:
|
|
(tmp_working_directory / "AGENTS.md").write_text(
|
|
"No repo decision here", encoding="utf-8"
|
|
)
|
|
|
|
with pytest.raises(InvalidRequestError):
|
|
await acp_agent_loop.ext_method(
|
|
"trust/decision",
|
|
{"cwd": str(tmp_working_directory), "decision": "trust_repo"},
|
|
)
|
|
|
|
with pytest.raises(InvalidRequestError):
|
|
await acp_agent_loop.ext_method(
|
|
"trust/decision",
|
|
{"cwd": str(tmp_working_directory), "decision": "trust_session"},
|
|
)
|
|
|
|
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_workspace_trust_decision_rejects_unknown_session_id(
|
|
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
|
|
) -> None:
|
|
(tmp_working_directory / "AGENTS.md").write_text(
|
|
"Unknown session", encoding="utf-8"
|
|
)
|
|
|
|
with pytest.raises(SessionNotFoundError):
|
|
await acp_agent_loop.ext_method(
|
|
"trust/decision",
|
|
{
|
|
"cwd": str(tmp_working_directory),
|
|
"decision": "trust_cwd",
|
|
"session_id": "missing-session",
|
|
},
|
|
)
|
|
|
|
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
|