vibe/tests/acp/test_fork_session.py
Mathias Gesbert 3f8487f761
v2.14.0 (#743)
Co-authored-by: Alexis Tacnet <alexis@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Val <102326092+vdeva@users.noreply.github.com>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com>
Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com>
Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-06-04 18:26:35 +02:00

192 lines
7.1 KiB
Python

from __future__ import annotations
import asyncio
from pathlib import Path
from acp.schema import TextContentBlock
import pytest
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.acp.exceptions import InvalidRequestError
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.session.session_id import extract_suffix
from vibe.core.types import FunctionCall, LLMMessage, Role, ToolCall
class TestACPForkSession:
@pytest.mark.asyncio
async def test_fork_session_clones_history_and_mode(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
source_session = acp_agent_loop.sessions[session_response.session_id]
await acp_agent_loop.set_session_mode(
session_id=source_session.id, mode_id=BuiltinAgentName.PLAN
)
await acp_agent_loop.prompt(
prompt=[TextContentBlock(type="text", text="Say hi")],
session_id=source_session.id,
)
response = await acp_agent_loop.fork_session(
cwd=str(Path.cwd()), session_id=source_session.id, mcp_servers=[]
)
assert response.session_id != source_session.id
assert response.modes is not None
assert response.modes.current_mode_id == BuiltinAgentName.PLAN
assert response.models is not None
assert (
response.models.current_model_id
== source_session.agent_loop.config.active_model
)
assert response.config_options is not None
assert len(response.config_options) == 3
forked_session = acp_agent_loop.sessions[response.session_id]
assert forked_session.agent_loop.agent_profile.name == BuiltinAgentName.PLAN
source_messages = [
message.model_dump(mode="json", exclude_none=True)
for message in source_session.agent_loop.messages
if message.role != Role.system
]
forked_messages = [
message.model_dump(mode="json", exclude_none=True)
for message in forked_session.agent_loop.messages
if message.role != Role.system
]
assert forked_messages == source_messages
@pytest.mark.asyncio
async def test_fork_session_from_user_message_keeps_full_turn(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
source_session = acp_agent_loop.sessions[session_response.session_id]
source_session.agent_loop.messages.extend([
LLMMessage(role=Role.user, content="First", message_id="user-1"),
LLMMessage(
role=Role.assistant, content="First answer", message_id="assistant-1"
),
LLMMessage(
role=Role.assistant,
content="",
message_id="assistant-2",
tool_calls=[
ToolCall(
id="call-1",
index=0,
function=FunctionCall(
name="read", arguments='{"file_path":"a.txt"}'
),
)
],
),
LLMMessage(role=Role.tool, content="contents", tool_call_id="call-1"),
LLMMessage(role=Role.user, content="Second", message_id="user-2"),
LLMMessage(
role=Role.assistant, content="Second answer", message_id="assistant-3"
),
])
response = await acp_agent_loop.fork_session(
cwd=str(Path.cwd()),
session_id=source_session.id,
mcp_servers=[],
messageId="user-1",
)
forked_session = acp_agent_loop.sessions[response.session_id]
assert [
(message.role, message.content, message.message_id, message.tool_call_id)
for message in forked_session.agent_loop.messages
if message.role != Role.system
] == [
(Role.user, "First", "user-1", None),
(Role.assistant, "First answer", "assistant-1", None),
(Role.assistant, "", "assistant-2", None),
(Role.tool, "contents", None, "call-1"),
]
@pytest.mark.asyncio
async def test_fork_session_rejects_non_user_message_id(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
source_session = acp_agent_loop.sessions[session_response.session_id]
source_session.agent_loop.messages.extend([
LLMMessage(role=Role.user, content="First", message_id="user-1"),
LLMMessage(
role=Role.assistant, content="First answer", message_id="assistant-1"
),
])
with pytest.raises(
InvalidRequestError,
match="Fork from message_id is only supported for user messages",
):
await acp_agent_loop.fork_session(
cwd=str(Path.cwd()),
session_id=source_session.id,
mcp_servers=[],
messageId="assistant-1",
)
@pytest.mark.asyncio
async def test_fork_session_sets_parent_session_id(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
source_session = acp_agent_loop.sessions[session_response.session_id]
response = await acp_agent_loop.fork_session(
cwd=str(Path.cwd()), session_id=source_session.id, mcp_servers=[]
)
forked_session = acp_agent_loop.sessions[response.session_id]
assert forked_session.agent_loop.parent_session_id == source_session.id
@pytest.mark.asyncio
async def test_fork_session_rejects_running_session(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
source_session = acp_agent_loop.sessions[session_response.session_id]
source_session.set_prompt_task(asyncio.sleep(10))
with pytest.raises(
InvalidRequestError,
match="Cannot fork a session while the agent loop is running",
):
await acp_agent_loop.fork_session(
cwd=str(Path.cwd()), session_id=source_session.id, mcp_servers=[]
)
await source_session.cancel_prompt()
@pytest.mark.asyncio
async def test_fork_session_preserves_session_id_suffix(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
source_session = acp_agent_loop.sessions[session_response.session_id]
response = await acp_agent_loop.fork_session(
cwd=str(Path.cwd()), session_id=source_session.id, mcp_servers=[]
)
assert extract_suffix(response.session_id) == extract_suffix(source_session.id)