v2.2.0 (#395)
Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai> Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
This commit is contained in:
parent
51fecc67d9
commit
ec7f3b25ea
107 changed files with 8002 additions and 535 deletions
|
|
@ -1,5 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -40,3 +43,55 @@ def acp_agent_loop(backend: FakeBackend) -> VibeAcpAgentLoop:
|
|||
|
||||
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgent).start()
|
||||
return _create_acp_agent()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_session_dir(tmp_path: Path) -> Path:
|
||||
session_dir = tmp_path / "sessions"
|
||||
session_dir.mkdir()
|
||||
return session_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_test_session():
|
||||
"""Create a test session with configurable messages and metadata.
|
||||
|
||||
Supports both messages parameter (for load_session tests) and
|
||||
end_time parameter (for list_sessions tests).
|
||||
"""
|
||||
|
||||
def _create_session(
|
||||
session_dir: Path,
|
||||
session_id: str,
|
||||
cwd: str,
|
||||
messages: list[dict] | None = None,
|
||||
title: str | None = None,
|
||||
end_time: str | None = None,
|
||||
) -> Path:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
session_folder = session_dir / f"session_{timestamp}_{session_id[:8]}"
|
||||
session_folder.mkdir(exist_ok=True)
|
||||
|
||||
if messages is None:
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
with messages_file.open("w", encoding="utf-8") as f:
|
||||
for msg in messages:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
|
||||
metadata = {
|
||||
"session_id": session_id,
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": end_time or "2024-01-01T12:05:00Z",
|
||||
"environment": {"working_directory": cwd},
|
||||
"title": title,
|
||||
}
|
||||
|
||||
metadata_file = session_folder / "meta.json"
|
||||
with metadata_file.open("w", encoding="utf-8") as f:
|
||||
json.dump(metadata, f)
|
||||
|
||||
return session_folder
|
||||
|
||||
return _create_session
|
||||
|
|
|
|||
|
|
@ -451,6 +451,18 @@ class TestSessionUpdates:
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
commands_response_text = await read_response(process)
|
||||
assert commands_response_text is not None
|
||||
commands_response = UpdateJsonRpcNotification.model_validate(
|
||||
json.loads(commands_response_text)
|
||||
)
|
||||
assert commands_response.params is not None
|
||||
assert (
|
||||
commands_response.params.update.session_update
|
||||
== "available_commands_update"
|
||||
)
|
||||
|
||||
user_response_text = await read_response(process)
|
||||
assert user_response_text is not None
|
||||
user_response = UpdateJsonRpcNotification.model_validate(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from acp.schema import (
|
|||
ClientCapabilities,
|
||||
Implementation,
|
||||
PromptCapabilities,
|
||||
SessionCapabilities,
|
||||
SessionListCapabilities,
|
||||
)
|
||||
import pytest
|
||||
|
||||
|
|
@ -19,13 +21,14 @@ class TestACPInitialize:
|
|||
|
||||
assert response.protocol_version == PROTOCOL_VERSION
|
||||
assert response.agent_capabilities == AgentCapabilities(
|
||||
load_session=False,
|
||||
load_session=True,
|
||||
prompt_capabilities=PromptCapabilities(
|
||||
audio=False, embedded_context=True, image=False
|
||||
),
|
||||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.1.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.2.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -42,13 +45,14 @@ class TestACPInitialize:
|
|||
|
||||
assert response.protocol_version == PROTOCOL_VERSION
|
||||
assert response.agent_capabilities == AgentCapabilities(
|
||||
load_session=False,
|
||||
load_session=True,
|
||||
prompt_capabilities=PromptCapabilities(
|
||||
audio=False, embedded_context=True, image=False
|
||||
),
|
||||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.1.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.2.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
242
tests/acp/test_list_sessions.py
Normal file
242
tests/acp/test_list_sessions.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.acp.conftest import _create_acp_agent
|
||||
from vibe.core.config import MissingAPIKeyError, SessionLoggingConfig
|
||||
|
||||
|
||||
class TestListSessions:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_empty(self, temp_session_dir: Path) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert response.sessions == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_returns_all_sessions(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"aaaaaaaa-1111",
|
||||
"/home/user/project1",
|
||||
title="First session",
|
||||
end_time="2024-01-01T12:00:00Z",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"bbbbbbbb-2222",
|
||||
"/home/user/project2",
|
||||
title="Second session",
|
||||
end_time="2024-01-01T13:00:00Z",
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert len(response.sessions) == 2
|
||||
session_ids = {s.session_id for s in response.sessions}
|
||||
assert "aaaaaaaa-1111" in session_ids
|
||||
assert "bbbbbbbb-2222" in session_ids
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_filters_by_cwd(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"aaaaaaaa-proj1",
|
||||
"/home/user/project1",
|
||||
title="Project 1 session",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"bbbbbbbb-proj2",
|
||||
"/home/user/project2",
|
||||
title="Project 2 session",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"cccccccc-proj1",
|
||||
"/home/user/project1",
|
||||
title="Another Project 1 session",
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions(cwd="/home/user/project1")
|
||||
|
||||
assert len(response.sessions) == 2
|
||||
for session in response.sessions:
|
||||
assert session.cwd == "/home/user/project1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_sorted_by_updated_at(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"oldest-s",
|
||||
"/home/user/project",
|
||||
title="Oldest",
|
||||
end_time="2024-01-01T10:00:00Z",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"newest-s",
|
||||
"/home/user/project",
|
||||
title="Newest",
|
||||
end_time="2024-01-01T14:00:00Z",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"middle-s",
|
||||
"/home/user/project",
|
||||
title="Middle",
|
||||
end_time="2024-01-01T12:00:00Z",
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert len(response.sessions) == 3
|
||||
|
||||
assert response.sessions[0].title == "Newest"
|
||||
assert response.sessions[1].title == "Middle"
|
||||
assert response.sessions[2].title == "Oldest"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_includes_session_info_fields(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"test-session-123",
|
||||
"/home/user/project",
|
||||
title="Test Session Title",
|
||||
end_time="2024-01-15T10:30:00Z",
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert len(response.sessions) == 1
|
||||
session = response.sessions[0]
|
||||
assert session.session_id == "test-session-123"
|
||||
assert session.cwd == "/home/user/project"
|
||||
assert session.title == "Test Session Title"
|
||||
# updated_at is normalized to UTC
|
||||
assert session.updated_at is not None
|
||||
assert session.updated_at.endswith("+00:00")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_skips_invalid_sessions(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir, "valid-se", "/home/user/project", title="Valid Session"
|
||||
)
|
||||
|
||||
invalid_session = temp_session_dir / "session_20240101_120000_invalid1"
|
||||
invalid_session.mkdir()
|
||||
(invalid_session / "meta.json").write_text('{"session_id": "invalid"}')
|
||||
|
||||
no_id_session = temp_session_dir / "session_20240101_120001_noid0000"
|
||||
no_id_session.mkdir()
|
||||
(no_id_session / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "Hello"}\n'
|
||||
)
|
||||
(no_id_session / "meta.json").write_text(
|
||||
'{"environment": {"working_directory": "/test"}}'
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert len(response.sessions) == 1
|
||||
assert response.sessions[0].session_id == "valid-se"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_nonexistent_save_dir(self) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir="/nonexistent/path", session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert response.sessions == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_without_api_key(self) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_load.side_effect = MissingAPIKeyError("api_key", "mistral")
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert response.sessions == []
|
||||
301
tests/acp/test_load_session.py
Normal file
301
tests/acp/test_load_session.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from acp import RequestError
|
||||
from acp.schema import (
|
||||
AgentMessageChunk,
|
||||
AgentThoughtChunk,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
UserMessageChunk,
|
||||
)
|
||||
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.core.agent_loop import AgentLoop
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config import ModelConfig, SessionLoggingConfig
|
||||
from vibe.core.types import Role
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_with_session_config(
|
||||
backend: FakeBackend, temp_session_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> tuple[VibeAcpAgentLoop, FakeClient]:
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
config = build_test_vibe_config(
|
||||
active_model="devstral-latest",
|
||||
models=[
|
||||
ModelConfig(
|
||||
name="devstral-latest", provider="mistral", alias="devstral-latest"
|
||||
)
|
||||
],
|
||||
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()
|
||||
|
||||
monkeypatch.setattr("vibe.acp.acp_agent_loop.AgentLoop", PatchedAgentLoop)
|
||||
monkeypatch.setattr(VibeAcpAgentLoop, "_load_config", lambda self: config)
|
||||
|
||||
vibe_acp_agent = VibeAcpAgentLoop()
|
||||
client = FakeClient()
|
||||
vibe_acp_agent.on_connect(client)
|
||||
client.on_connect(vibe_acp_agent)
|
||||
|
||||
return vibe_acp_agent, client
|
||||
|
||||
|
||||
class TestLoadSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_returns_response_with_models_and_modes(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
session_id = "test-sess-12345678"
|
||||
cwd = str(Path.cwd())
|
||||
create_test_session(temp_session_dir, session_id, cwd)
|
||||
|
||||
response = await acp_agent.load_session(
|
||||
cwd=cwd, mcp_servers=[], session_id=session_id
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.models is not None
|
||||
assert response.models.current_model_id == "devstral-latest"
|
||||
assert response.modes is not None
|
||||
assert response.modes.current_mode_id == BuiltinAgentName.DEFAULT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_registers_session_with_original_id(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
session_id = "orig-id-12345678"
|
||||
cwd = str(Path.cwd())
|
||||
create_test_session(temp_session_dir, session_id, cwd)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
assert session_id in acp_agent.sessions
|
||||
assert acp_agent.sessions[session_id].id == session_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_injects_messages_into_agent_loop(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
session_id = "msg-test-12345678"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "system", "content": "You are helpful"},
|
||||
{"role": "user", "content": "First question"},
|
||||
{"role": "assistant", "content": "First answer"},
|
||||
{"role": "user", "content": "Second question"},
|
||||
{"role": "assistant", "content": "Second answer"},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
session = acp_agent.sessions[session_id]
|
||||
|
||||
non_system = [m for m in session.agent_loop.messages if m.role != Role.system]
|
||||
assert len(non_system) == 4
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_user_messages(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-usr-123456"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [{"role": "user", "content": "Hello world"}]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
user_updates = [
|
||||
u for u in client._session_updates if isinstance(u.update, UserMessageChunk)
|
||||
]
|
||||
assert len(user_updates) == 1
|
||||
assert user_updates[0].update.content.text == "Hello world"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_assistant_messages(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-ast-123456"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "Hi"},
|
||||
{"role": "assistant", "content": "Hello! How can I help?"},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
agent_updates = [
|
||||
u
|
||||
for u in client._session_updates
|
||||
if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
assert len(agent_updates) == 1
|
||||
assert agent_updates[0].update.content.text == "Hello! How can I help?"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_tool_calls(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-tool-12345"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "Read the file"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": '{"path": "/tmp/test.txt"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_123", "content": "file contents"},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
tool_call_starts = [
|
||||
u for u in client._session_updates if isinstance(u.update, ToolCallStart)
|
||||
]
|
||||
assert len(tool_call_starts) == 1
|
||||
assert tool_call_starts[0].update.title == "read_file"
|
||||
assert tool_call_starts[0].update.tool_call_id == "call_123"
|
||||
|
||||
tool_results = [
|
||||
u for u in client._session_updates if isinstance(u.update, ToolCallProgress)
|
||||
]
|
||||
assert len(tool_results) == 1
|
||||
assert tool_results[0].update.tool_call_id == "call_123"
|
||||
assert tool_results[0].update.status == "completed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_reasoning_content(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-reason-123"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "Think about this"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Here is my answer",
|
||||
"reasoning_content": "Let me think step by step...",
|
||||
},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
thought_updates = [
|
||||
u
|
||||
for u in client._session_updates
|
||||
if isinstance(u.update, AgentThoughtChunk)
|
||||
]
|
||||
assert len(thought_updates) == 1
|
||||
assert thought_updates[0].update.content.text == "Let me think step by step..."
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_not_found_raises_error(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
with pytest.raises(RequestError):
|
||||
await acp_agent.load_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[], session_id="nonexistent-session"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_full_conversation(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "full-conv-1234567"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "First message"},
|
||||
{"role": "assistant", "content": "First response"},
|
||||
{"role": "user", "content": "Second message"},
|
||||
{"role": "assistant", "content": "Second response"},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
user_updates = [
|
||||
u for u in client._session_updates if isinstance(u.update, UserMessageChunk)
|
||||
]
|
||||
agent_updates = [
|
||||
u
|
||||
for u in client._session_updates
|
||||
if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(user_updates) == 2
|
||||
assert len(agent_updates) == 2
|
||||
assert user_updates[0].update.content.text == "First message"
|
||||
assert user_updates[1].update.content.text == "Second message"
|
||||
assert agent_updates[0].update.content.text == "First response"
|
||||
assert agent_updates[1].update.content.text == "Second response"
|
||||
|
|
@ -41,12 +41,18 @@ def acp_agent_loop(backend) -> VibeAcpAgentLoop:
|
|||
class TestACPNewSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_session_response_structure(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict]
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
|
||||
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 session_response.session_id is not None
|
||||
acp_session = next(
|
||||
(
|
||||
|
|
|
|||
271
tests/acp/test_proxy_setup_acp.py
Normal file
271
tests/acp/test_proxy_setup_acp.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from acp.schema import AgentMessageChunk, AvailableCommandsUpdate, TextContentBlock
|
||||
import pytest
|
||||
|
||||
from tests.acp.conftest import _create_acp_agent
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from tests.stubs.fake_client import FakeClient
|
||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_loop(backend) -> 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(acp_agent_loop: VibeAcpAgentLoop) -> FakeClient:
|
||||
assert isinstance(acp_agent_loop.client, FakeClient)
|
||||
return acp_agent_loop.client
|
||||
|
||||
|
||||
class TestAvailableCommandsUpdate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_available_commands_sent_on_new_session(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
import asyncio
|
||||
|
||||
await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
|
||||
await asyncio.sleep(0)
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
available_commands_updates = [
|
||||
u for u in updates if isinstance(u.update, AvailableCommandsUpdate)
|
||||
]
|
||||
|
||||
assert len(available_commands_updates) == 1
|
||||
update = available_commands_updates[0].update
|
||||
assert len(update.available_commands) == 1
|
||||
assert update.available_commands[0].name == "proxy-setup"
|
||||
assert "proxy" in update.available_commands[0].description.lower()
|
||||
|
||||
|
||||
class TestProxySetupCommand:
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_shows_help_when_no_args(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[TextContentBlock(type="text", text="/proxy-setup")],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
message_updates = [
|
||||
u for u in updates if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(message_updates) == 1
|
||||
content = message_updates[0].update.content.text
|
||||
assert "## Proxy Configuration" in content
|
||||
assert "HTTP_PROXY" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_sets_value(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[
|
||||
TextContentBlock(
|
||||
type="text", text="/proxy-setup HTTP_PROXY http://localhost:8080"
|
||||
)
|
||||
],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
message_updates = [
|
||||
u for u in updates if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(message_updates) == 1
|
||||
content = message_updates[0].update.content.text
|
||||
assert "HTTP_PROXY" in content
|
||||
assert "http://localhost:8080" in content
|
||||
|
||||
assert env_file.exists()
|
||||
env_content = env_file.read_text()
|
||||
assert "HTTP_PROXY" in env_content
|
||||
assert "http://localhost:8080" in env_content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_unsets_value(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("HTTP_PROXY=http://old-proxy.com\n")
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[TextContentBlock(type="text", text="/proxy-setup HTTP_PROXY")],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
message_updates = [
|
||||
u for u in updates if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(message_updates) == 1
|
||||
content = message_updates[0].update.content.text
|
||||
assert "Removed" in content
|
||||
assert "HTTP_PROXY" in content
|
||||
|
||||
env_content = env_file.read_text()
|
||||
assert "HTTP_PROXY" not in env_content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_invalid_key_returns_error(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[
|
||||
TextContentBlock(type="text", text="/proxy-setup INVALID_KEY value")
|
||||
],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
message_updates = [
|
||||
u for u in updates if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(message_updates) == 1
|
||||
content = message_updates[0].update.content.text
|
||||
assert "Error" in content
|
||||
assert "Unknown key" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_case_insensitive(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[
|
||||
TextContentBlock(
|
||||
type="text", text="/PROXY-SETUP http_proxy http://localhost:8080"
|
||||
)
|
||||
],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
assert env_file.exists()
|
||||
env_content = env_file.read_text()
|
||||
assert "HTTP_PROXY" in env_content
|
||||
55
tests/acp/test_utils.py
Normal file
55
tests/acp/test_utils.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.acp.utils import get_proxy_help_text
|
||||
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
|
||||
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS
|
||||
|
||||
|
||||
def _write_env_file(content: str) -> None:
|
||||
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
GLOBAL_ENV_FILE.path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
class TestGetProxyHelpText:
|
||||
def test_returns_string(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_includes_proxy_configuration_header(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "## Proxy Configuration" in result
|
||||
|
||||
def test_includes_usage_section(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "### Usage:" in result
|
||||
assert "/proxy-setup" in result
|
||||
|
||||
def test_includes_all_supported_variables(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
for key in SUPPORTED_PROXY_VARS:
|
||||
assert f"`{key}`" in result
|
||||
|
||||
def test_shows_none_configured_when_no_settings(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "(none configured)" in result
|
||||
|
||||
def test_shows_current_settings_when_configured(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
||||
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "HTTP_PROXY=http://proxy:8080" in result
|
||||
assert "(none configured)" not in result
|
||||
|
||||
def test_shows_only_set_values(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
||||
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "HTTP_PROXY=http://proxy:8080" in result
|
||||
assert "HTTPS_PROXY=" not in result
|
||||
Loading…
Add table
Add a link
Reference in a new issue