v2.14.1 (#756)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Gauthier Guinet <43207538+Gguinet@users.noreply.github.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Stanislas <stanislas.lange@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
3f8487f761
commit
702d0f412e
51 changed files with 2446 additions and 502 deletions
|
|
@ -3,10 +3,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from acp.schema import AgentMessageChunk, AvailableCommandsUpdate, TextContentBlock
|
||||
from acp.schema import (
|
||||
AgentMessageChunk,
|
||||
AllowedOutcome,
|
||||
AvailableCommandsUpdate,
|
||||
DeniedOutcome,
|
||||
RequestPermissionResponse,
|
||||
TextContentBlock,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from tests.acp.conftest import _create_acp_agent
|
||||
|
|
@ -14,8 +25,20 @@ from tests.skills.conftest import create_skill
|
|||
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.teleport import TELEPORT_PUSH_OPTION_ID
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.config import SessionLoggingConfig
|
||||
from vibe.core.teleport.errors import ServiceTeleportError
|
||||
from vibe.core.teleport.teleport import TeleportService
|
||||
from vibe.core.teleport.types import (
|
||||
TeleportCheckingGitEvent,
|
||||
TeleportCompleteEvent,
|
||||
TeleportPushingEvent,
|
||||
TeleportPushRequiredEvent,
|
||||
TeleportPushResponseEvent,
|
||||
TeleportStartingWorkflowEvent,
|
||||
)
|
||||
from vibe.core.types import LLMMessage, Role
|
||||
|
||||
|
||||
def _get_client(agent: VibeAcpAgentLoop) -> FakeClient:
|
||||
|
|
@ -32,14 +55,37 @@ def _get_message_texts(agent: VibeAcpAgentLoop) -> list[str]:
|
|||
]
|
||||
|
||||
|
||||
def _get_tool_updates(
|
||||
agent: VibeAcpAgentLoop,
|
||||
) -> list[ToolCallStart | ToolCallProgress]:
|
||||
return [
|
||||
u.update
|
||||
for u in _get_client(agent)._session_updates
|
||||
if isinstance(u.update, (ToolCallStart, ToolCallProgress))
|
||||
]
|
||||
|
||||
|
||||
def _set_teleport_service(agent_loop: AgentLoop, service: object) -> None:
|
||||
agent_loop._teleport_service = cast(TeleportService, service)
|
||||
|
||||
|
||||
async def _new_session_and_clear(agent: VibeAcpAgentLoop) -> str:
|
||||
"""Create a new session, drain the startup updates, return session_id."""
|
||||
resp = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
await asyncio.sleep(0) # let background tasks (available_commands) complete
|
||||
await _wait_for_available_commands(agent)
|
||||
_get_client(agent)._session_updates.clear()
|
||||
return resp.session_id
|
||||
|
||||
|
||||
async def _wait_for_available_commands(agent: VibeAcpAgentLoop) -> None:
|
||||
for _ in range(50):
|
||||
updates = _get_client(agent)._session_updates
|
||||
if any(isinstance(u.update, AvailableCommandsUpdate) for u in updates):
|
||||
return
|
||||
await asyncio.sleep(0.01)
|
||||
raise TimeoutError("available commands update was not sent")
|
||||
|
||||
|
||||
async def _prompt(agent: VibeAcpAgentLoop, session_id: str, text: str):
|
||||
return await agent.prompt(
|
||||
prompt=[TextContentBlock(type="text", text=text)], session_id=session_id
|
||||
|
|
@ -51,6 +97,7 @@ def _make_patched_agent_loop(
|
|||
*,
|
||||
skill_paths: list[Path] | None = None,
|
||||
session_logging: SessionLoggingConfig | None = None,
|
||||
vibe_code_enabled: bool | None = None,
|
||||
) -> type[AgentLoop]:
|
||||
"""Create a PatchedAgentLoop class that injects config overrides."""
|
||||
config_updates: dict = {}
|
||||
|
|
@ -58,6 +105,8 @@ def _make_patched_agent_loop(
|
|||
config_updates["skill_paths"] = skill_paths
|
||||
if session_logging is not None:
|
||||
config_updates["session_logging"] = session_logging
|
||||
if vibe_code_enabled is not None:
|
||||
config_updates["vibe_code_enabled"] = vibe_code_enabled
|
||||
|
||||
class PatchedAgentLoop(AgentLoop):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
|
|
@ -75,6 +124,13 @@ def acp_agent_loop(backend: FakeBackend) -> VibeAcpAgentLoop:
|
|||
return _create_acp_agent()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_loop_vibe_code_disabled(backend: FakeBackend) -> VibeAcpAgentLoop:
|
||||
patched = _make_patched_agent_loop(backend, vibe_code_enabled=False)
|
||||
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=patched).start()
|
||||
return _create_acp_agent()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def skills_dir(tmp_path: Path) -> Path:
|
||||
d = tmp_path / "skills"
|
||||
|
|
@ -159,6 +215,264 @@ class TestHandleCompact:
|
|||
mock_compact.assert_called_once()
|
||||
|
||||
|
||||
class TestHandleTeleport:
|
||||
@pytest.mark.asyncio
|
||||
async def test_available_commands_includes_teleport(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
await _wait_for_available_commands(acp_agent_loop)
|
||||
|
||||
available = [
|
||||
u
|
||||
for u in _get_client(acp_agent_loop)._session_updates
|
||||
if isinstance(u.update, AvailableCommandsUpdate)
|
||||
]
|
||||
cmd_names = [c.name for c in available[0].update.available_commands]
|
||||
|
||||
assert "teleport" in cmd_names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_hidden_when_vibe_code_disabled(
|
||||
self, acp_agent_loop_vibe_code_disabled: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
agent = acp_agent_loop_vibe_code_disabled
|
||||
await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
await _wait_for_available_commands(agent)
|
||||
|
||||
available = [
|
||||
u
|
||||
for u in _get_client(agent)._session_updates
|
||||
if isinstance(u.update, AvailableCommandsUpdate)
|
||||
]
|
||||
cmd_names = [c.name for c in available[0].update.available_commands]
|
||||
|
||||
assert "teleport" not in cmd_names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_without_history_replies_with_no_history(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
|
||||
response = await _prompt(acp_agent_loop, session_id, "/teleport")
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
assert response.field_meta == {
|
||||
"tool_name": "teleport",
|
||||
"teleport": {"status": "no_history"},
|
||||
}
|
||||
assert _get_message_texts(acp_agent_loop) == [
|
||||
"No conversation history to teleport."
|
||||
]
|
||||
assert _get_tool_updates(acp_agent_loop) == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_sends_tool_updates_and_structured_url(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
class FakeTeleportService:
|
||||
prompts: list[str]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.prompts = []
|
||||
|
||||
async def __aenter__(self) -> FakeTeleportService:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def execute(self, prompt: str) -> AsyncGenerator[object, object]:
|
||||
self.prompts.append(prompt)
|
||||
yield TeleportCheckingGitEvent()
|
||||
yield TeleportStartingWorkflowEvent()
|
||||
yield TeleportCompleteEvent(url="https://chat.example.com/code/1/2")
|
||||
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
session = acp_agent_loop.sessions[session_id]
|
||||
session.agent_loop.messages.append(
|
||||
LLMMessage(role=Role.user, content="continue this task")
|
||||
)
|
||||
service = FakeTeleportService()
|
||||
_set_teleport_service(session.agent_loop, service)
|
||||
|
||||
response = await _prompt(acp_agent_loop, session_id, "/teleport ignored")
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
assert response.field_meta == {
|
||||
"tool_name": "teleport",
|
||||
"teleport": {
|
||||
"status": "completed",
|
||||
"url": "https://chat.example.com/code/1/2",
|
||||
},
|
||||
}
|
||||
assert service.prompts == ["continue this task (continue)"]
|
||||
assert _get_message_texts(acp_agent_loop) == []
|
||||
|
||||
tool_updates = _get_tool_updates(acp_agent_loop)
|
||||
assert isinstance(tool_updates[0], ToolCallStart)
|
||||
assert tool_updates[0].title == "Teleporting session to Vibe Code Web..."
|
||||
assert tool_updates[-1].status == "completed"
|
||||
assert tool_updates[-1].title == "Teleported to Vibe Code Web"
|
||||
assert [update.field_meta for update in tool_updates] == [
|
||||
{"tool_name": "teleport", "teleport": {"status": "starting"}},
|
||||
{"tool_name": "teleport", "teleport": {"status": "preparing_workspace"}},
|
||||
{"tool_name": "teleport", "teleport": {"status": "starting_workflow"}},
|
||||
{
|
||||
"tool_name": "teleport",
|
||||
"teleport": {
|
||||
"status": "completed",
|
||||
"url": "https://chat.example.com/code/1/2",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_push_required_requests_permission(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
class FakeTeleportService:
|
||||
response: TeleportPushResponseEvent | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.response = None
|
||||
|
||||
async def __aenter__(self) -> FakeTeleportService:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def execute(self, prompt: str) -> AsyncGenerator[object, object]:
|
||||
yield TeleportCheckingGitEvent()
|
||||
response = yield TeleportPushRequiredEvent(
|
||||
unpushed_count=2, branch_not_pushed=False
|
||||
)
|
||||
self.response = cast(TeleportPushResponseEvent, response)
|
||||
yield TeleportPushingEvent()
|
||||
yield TeleportCompleteEvent(url="https://chat.example.com/code/1/2")
|
||||
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
session = acp_agent_loop.sessions[session_id]
|
||||
session.agent_loop.messages.append(
|
||||
LLMMessage(role=Role.user, content="ship it")
|
||||
)
|
||||
service = FakeTeleportService()
|
||||
_set_teleport_service(session.agent_loop, service)
|
||||
|
||||
client = _get_client(acp_agent_loop)
|
||||
client.request_permission = AsyncMock(
|
||||
return_value=RequestPermissionResponse(
|
||||
outcome=AllowedOutcome(
|
||||
outcome="selected", option_id=TELEPORT_PUSH_OPTION_ID
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
response = await _prompt(acp_agent_loop, session_id, "/teleport")
|
||||
|
||||
assert response.field_meta == {
|
||||
"tool_name": "teleport",
|
||||
"teleport": {
|
||||
"status": "completed",
|
||||
"url": "https://chat.example.com/code/1/2",
|
||||
},
|
||||
}
|
||||
assert service.response == TeleportPushResponseEvent(approved=True)
|
||||
request_permission = cast(AsyncMock, client.request_permission)
|
||||
request_permission.assert_awaited_once()
|
||||
await_args = request_permission.await_args
|
||||
assert await_args is not None
|
||||
kwargs = await_args.kwargs
|
||||
assert (
|
||||
kwargs["tool_call"].title
|
||||
== "You have 2 unpushed commits. Push to continue?"
|
||||
)
|
||||
assert kwargs["tool_call"].field_meta == {
|
||||
"tool_name": "teleport",
|
||||
"teleport": {
|
||||
"status": "push_required",
|
||||
"unpushedCount": 2,
|
||||
"branchNotPushed": False,
|
||||
},
|
||||
}
|
||||
assert [option.name for option in kwargs["options"]] == [
|
||||
"Push and continue",
|
||||
"Cancel",
|
||||
]
|
||||
assert [update.field_meta for update in _get_tool_updates(acp_agent_loop)] == [
|
||||
{"tool_name": "teleport", "teleport": {"status": "starting"}},
|
||||
{"tool_name": "teleport", "teleport": {"status": "preparing_workspace"}},
|
||||
{
|
||||
"tool_name": "teleport",
|
||||
"teleport": {
|
||||
"status": "push_required",
|
||||
"unpushedCount": 2,
|
||||
"branchNotPushed": False,
|
||||
},
|
||||
},
|
||||
{"tool_name": "teleport", "teleport": {"status": "syncing_remote"}},
|
||||
{
|
||||
"tool_name": "teleport",
|
||||
"teleport": {
|
||||
"status": "completed",
|
||||
"url": "https://chat.example.com/code/1/2",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_push_denied_marks_tool_call_failed(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
class FakeTeleportService:
|
||||
async def __aenter__(self) -> FakeTeleportService:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def execute(self, prompt: str) -> AsyncGenerator[object, object]:
|
||||
response = yield TeleportPushRequiredEvent()
|
||||
if (
|
||||
not isinstance(response, TeleportPushResponseEvent)
|
||||
or not response.approved
|
||||
):
|
||||
raise ServiceTeleportError(
|
||||
"Teleport cancelled: changes not pushed."
|
||||
)
|
||||
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
session = acp_agent_loop.sessions[session_id]
|
||||
session.agent_loop.messages.append(
|
||||
LLMMessage(role=Role.user, content="ship it")
|
||||
)
|
||||
_set_teleport_service(session.agent_loop, FakeTeleportService())
|
||||
|
||||
client = _get_client(acp_agent_loop)
|
||||
client.request_permission = AsyncMock(
|
||||
return_value=RequestPermissionResponse(
|
||||
outcome=DeniedOutcome(outcome="cancelled")
|
||||
)
|
||||
)
|
||||
|
||||
response = await _prompt(acp_agent_loop, session_id, "/teleport")
|
||||
|
||||
assert response.field_meta == {
|
||||
"tool_name": "teleport",
|
||||
"teleport": {"status": "failed"},
|
||||
}
|
||||
failed = _get_tool_updates(acp_agent_loop)[-1]
|
||||
assert failed.status == "failed"
|
||||
assert failed.title == "Teleport failed"
|
||||
assert failed.raw_output == "Teleport cancelled: changes not pushed."
|
||||
assert failed.field_meta == {
|
||||
"tool_name": "teleport",
|
||||
"teleport": {"status": "failed"},
|
||||
}
|
||||
|
||||
|
||||
class TestHandleReload:
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_calls_reload_with_initial_messages(
|
||||
|
|
@ -213,6 +527,19 @@ class TestCommandFallthrough:
|
|||
texts = _get_message_texts(acp_agent_loop)
|
||||
assert any("Hi" in t for t in texts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ampersand_message_reaches_agent(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
response = await _prompt(acp_agent_loop, session_id, "&fix from here")
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
assert response.field_meta is None
|
||||
assert _get_tool_updates(acp_agent_loop) == []
|
||||
texts = _get_message_texts(acp_agent_loop)
|
||||
assert any("Hi" in t for t in texts)
|
||||
|
||||
|
||||
class TestAvailableCommandsWithSkills:
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -224,7 +551,7 @@ class TestAvailableCommandsWithSkills:
|
|||
await acp_agent_loop_with_skills.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
await _wait_for_available_commands(acp_agent_loop_with_skills)
|
||||
|
||||
updates = _get_client(acp_agent_loop_with_skills)._session_updates
|
||||
available = [
|
||||
|
|
@ -247,7 +574,7 @@ class TestAvailableCommandsWithSkills:
|
|||
await acp_agent_loop_with_skills.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
await _wait_for_available_commands(acp_agent_loop_with_skills)
|
||||
|
||||
updates = _get_client(acp_agent_loop_with_skills)._session_updates
|
||||
available = [
|
||||
|
|
|
|||
|
|
@ -43,9 +43,14 @@ def build_acp_agent_loop(*, provider: ProviderConfig | None = None) -> VibeAcpAg
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unauthenticated_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
|
||||
|
||||
|
||||
class TestACPInitialize:
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize(self) -> None:
|
||||
async def test_initialize(self, unauthenticated_env: None) -> None:
|
||||
acp_agent_loop = build_acp_agent_loop()
|
||||
response = await acp_agent_loop.initialize(protocol_version=PROTOCOL_VERSION)
|
||||
|
||||
|
|
@ -62,7 +67,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.1"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
@ -73,7 +78,9 @@ class TestACPInitialize:
|
|||
assert auth_method.description == BROWSER_AUTH_DESCRIPTION
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_with_terminal_auth(self) -> None:
|
||||
async def test_initialize_with_terminal_auth(
|
||||
self, unauthenticated_env: None
|
||||
) -> None:
|
||||
"""Test initialize with terminal-auth capabilities to check it was included."""
|
||||
acp_agent_loop = build_acp_agent_loop()
|
||||
client_capabilities = ClientCapabilities(field_meta={"terminal-auth": True})
|
||||
|
|
@ -94,7 +101,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.1"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
@ -109,6 +116,8 @@ class TestACPInitialize:
|
|||
assert auth_method.id == "vibe-setup"
|
||||
assert auth_method.name == "Register your API Key"
|
||||
assert auth_method.description == "Register your API Key inside Mistral Vibe"
|
||||
assert auth_method.args is not None
|
||||
assert auth_method.args[-1:] == ["--setup"]
|
||||
assert auth_method.field_meta is not None
|
||||
assert "terminal-auth" in auth_method.field_meta
|
||||
terminal_auth_meta = auth_method.field_meta["terminal-auth"]
|
||||
|
|
@ -118,7 +127,9 @@ class TestACPInitialize:
|
|||
assert terminal_auth_meta["label"] == "Mistral Vibe Setup"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_with_delegated_browser_auth(self) -> None:
|
||||
async def test_initialize_with_delegated_browser_auth(
|
||||
self, unauthenticated_env: None
|
||||
) -> None:
|
||||
acp_agent_loop = build_acp_agent_loop()
|
||||
client_capabilities = ClientCapabilities(
|
||||
field_meta={"browser-auth-delegated": True}
|
||||
|
|
@ -156,3 +167,43 @@ class TestACPInitialize:
|
|||
response = await acp_agent_loop.initialize(protocol_version=PROTOCOL_VERSION)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_omits_auth_methods_for_authenticated_jetbrains_client(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "test-api-key")
|
||||
acp_agent_loop = build_acp_agent_loop()
|
||||
client_capabilities = ClientCapabilities(
|
||||
field_meta={"terminal-auth": True, "browser-auth-delegated": True}
|
||||
)
|
||||
client_info = Implementation(name="JetBrains.PyCharm", version="2026.1.2")
|
||||
|
||||
response = await acp_agent_loop.initialize(
|
||||
protocol_version=PROTOCOL_VERSION,
|
||||
client_capabilities=client_capabilities,
|
||||
client_info=client_info,
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_keeps_auth_methods_for_authenticated_non_jetbrains_client(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "test-api-key")
|
||||
acp_agent_loop = build_acp_agent_loop()
|
||||
client_capabilities = ClientCapabilities(field_meta={"terminal-auth": True})
|
||||
client_info = Implementation(name="zed", version="0.999.0")
|
||||
|
||||
response = await acp_agent_loop.initialize(
|
||||
protocol_version=PROTOCOL_VERSION,
|
||||
client_capabilities=client_capabilities,
|
||||
client_info=client_info,
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
assert {method.id for method in response.auth_methods} == {
|
||||
"browser-auth",
|
||||
"vibe-setup",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,14 +34,34 @@ def _get_fake_client(acp_agent_loop: VibeAcpAgentLoop) -> FakeClient:
|
|||
return acp_agent_loop.client
|
||||
|
||||
|
||||
async def _wait_for_available_commands(acp_agent_loop: VibeAcpAgentLoop) -> None:
|
||||
for _ in range(50):
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
if any(isinstance(u.update, AvailableCommandsUpdate) for u in updates):
|
||||
return
|
||||
await asyncio.sleep(0.01)
|
||||
raise TimeoutError("available commands update was not sent")
|
||||
|
||||
|
||||
class TestAvailableCommandsUpdate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_initial_available_commands_are_delayed_until_after_new_session(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
assert not any(isinstance(u.update, AvailableCommandsUpdate) for u in updates)
|
||||
|
||||
await _wait_for_available_commands(acp_agent_loop)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_available_commands_sent_on_new_session(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
|
||||
await asyncio.sleep(0)
|
||||
await _wait_for_available_commands(acp_agent_loop)
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
available_commands_updates = [
|
||||
|
|
@ -62,7 +82,7 @@ class TestAvailableCommandsUpdate:
|
|||
) -> None:
|
||||
await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
|
||||
await asyncio.sleep(0)
|
||||
await _wait_for_available_commands(acp_agent_loop)
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
available_commands_updates = [
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ 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, VibeConfig
|
||||
from vibe.core.middleware import TurnLimitMiddleware
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -399,3 +400,131 @@ class TestACPSetConfigOptionThinking:
|
|||
)
|
||||
|
||||
assert response is None
|
||||
|
||||
|
||||
class TestACPSetConfigOptionMaxTurns:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_max_turns_success(
|
||||
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
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="max_turns", value="100"
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert acp_session.agent_loop._max_turns == 100
|
||||
turn_limits = [
|
||||
m
|
||||
for m in acp_session.agent_loop.middleware_pipeline.middlewares
|
||||
if isinstance(m, TurnLimitMiddleware)
|
||||
]
|
||||
assert len(turn_limits) == 1
|
||||
assert turn_limits[0].max_turns == 100
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_max_turns_string_value(
|
||||
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
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="max_turns", value="50"
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert acp_session.agent_loop._max_turns == 50
|
||||
turn_limits = [
|
||||
m
|
||||
for m in acp_session.agent_loop.middleware_pipeline.middlewares
|
||||
if isinstance(m, TurnLimitMiddleware)
|
||||
]
|
||||
assert len(turn_limits) == 1
|
||||
assert turn_limits[0].max_turns == 50
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_max_turns_invalid_string_returns_none(
|
||||
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
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
initial_max_turns = acp_session.agent_loop._max_turns
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="max_turns", value="abc"
|
||||
)
|
||||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop._max_turns == initial_max_turns
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_max_turns_bool_returns_none(
|
||||
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
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
initial_max_turns = acp_session.agent_loop._max_turns
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="max_turns", value=True
|
||||
)
|
||||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop._max_turns == initial_max_turns
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_max_turns_repeated_set(
|
||||
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
|
||||
acp_session = next(
|
||||
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
|
||||
)
|
||||
assert acp_session is not None
|
||||
|
||||
await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="max_turns", value="100"
|
||||
)
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="max_turns", value="200"
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert acp_session.agent_loop._max_turns == 200
|
||||
turn_limits = [
|
||||
m
|
||||
for m in acp_session.agent_loop.middleware_pipeline.middlewares
|
||||
if isinstance(m, TurnLimitMiddleware)
|
||||
]
|
||||
assert len(turn_limits) == 1
|
||||
assert turn_limits[0].max_turns == 200
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue