v2.9.0 (#641)
Co-authored-by: Antoine <33425718+anth2o@users.noreply.github.com> Co-authored-by: Bastien <bastien.baret@gmail.com> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Robin Gullo <robin.gullo@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
a83c81ecf5
commit
632ea8c032
253 changed files with 13965 additions and 2525 deletions
|
|
@ -67,6 +67,7 @@ def create_test_session():
|
|||
messages: list[dict] | None = None,
|
||||
title: str | None = None,
|
||||
end_time: str | None = None,
|
||||
parent_session_id: str | None = None,
|
||||
) -> Path:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
session_folder = session_dir / f"session_{timestamp}_{session_id[:8]}"
|
||||
|
|
@ -84,9 +85,14 @@ def create_test_session():
|
|||
"session_id": session_id,
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": end_time or "2024-01-01T12:05:00Z",
|
||||
"git_commit": None,
|
||||
"git_branch": None,
|
||||
"username": "test-user",
|
||||
"environment": {"working_directory": cwd},
|
||||
"title": title,
|
||||
}
|
||||
if parent_session_id is not None:
|
||||
metadata["parent_session_id"] = parent_session_id
|
||||
|
||||
metadata_file = session_folder / "meta.json"
|
||||
with metadata_file.open("w", encoding="utf-8") as f:
|
||||
|
|
|
|||
|
|
@ -513,12 +513,11 @@ class TestSessionUpdates:
|
|||
),
|
||||
),
|
||||
)
|
||||
text_responses = await read_multiple_responses(process, max_count=10)
|
||||
text_responses = await read_multiple_responses(
|
||||
process, max_count=15, timeout_per_response=2.0
|
||||
)
|
||||
assert len(text_responses) > 0
|
||||
responses = [
|
||||
UpdateJsonRpcNotification.model_validate(json.loads(r))
|
||||
for r in text_responses
|
||||
]
|
||||
responses = parse_conversation(text_responses)
|
||||
|
||||
tool_call = next(
|
||||
(
|
||||
|
|
|
|||
171
tests/acp/test_acp_hooks.py
Normal file
171
tests/acp/test_acp_hooks.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import tomli_w
|
||||
|
||||
from tests.conftest import get_base_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.hooks.models import HookConfigResult, HookType
|
||||
from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role, SessionMetadata
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def backend() -> FakeBackend:
|
||||
return FakeBackend(
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _write_config(config_dir: Path, *, enable_hooks: bool) -> None:
|
||||
config = get_base_config()
|
||||
config["enable_experimental_hooks"] = enable_hooks
|
||||
with (config_dir / "config.toml").open("wb") as f:
|
||||
tomli_w.dump(config, f)
|
||||
|
||||
|
||||
def _create_acp_agent() -> VibeAcpAgentLoop:
|
||||
agent = VibeAcpAgentLoop()
|
||||
client = FakeClient()
|
||||
agent.on_connect(client)
|
||||
client.on_connect(agent)
|
||||
return agent
|
||||
|
||||
|
||||
def _spy_agent_loop(backend: FakeBackend) -> tuple[MagicMock, type[AgentLoop]]:
|
||||
spy = MagicMock()
|
||||
|
||||
class Patched(AgentLoop):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
spy(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs, backend=backend)
|
||||
|
||||
return spy, Patched
|
||||
|
||||
|
||||
def _hook_config_from_spy(spy: MagicMock) -> HookConfigResult | None:
|
||||
hook_config_result = spy.call_args.kwargs["hook_config_result"]
|
||||
if hook_config_result is None:
|
||||
return None
|
||||
assert isinstance(hook_config_result, HookConfigResult)
|
||||
return hook_config_result
|
||||
|
||||
|
||||
async def _new_session(backend: FakeBackend) -> MagicMock:
|
||||
spy, patched_loop = _spy_agent_loop(backend)
|
||||
acp = _create_acp_agent()
|
||||
with patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=patched_loop):
|
||||
await acp.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
spy.assert_called_once()
|
||||
return spy
|
||||
|
||||
|
||||
async def _load_session(
|
||||
backend: FakeBackend,
|
||||
tmp_path: Path,
|
||||
*,
|
||||
session_id: str = "session-id",
|
||||
loaded_messages: list[LLMMessage] | None = None,
|
||||
) -> MagicMock:
|
||||
spy, patched_loop = _spy_agent_loop(backend)
|
||||
acp = _create_acp_agent()
|
||||
session_dir = tmp_path / "sessions" / session_id
|
||||
session_dir.mkdir(parents=True)
|
||||
disk_metadata = SessionMetadata(
|
||||
session_id=session_id,
|
||||
start_time="2024-01-01T12:00:00Z",
|
||||
end_time="2024-01-01T12:05:00Z",
|
||||
git_commit=None,
|
||||
git_branch=None,
|
||||
environment={"working_directory": str(Path.cwd())},
|
||||
username="test-user",
|
||||
)
|
||||
(session_dir / "meta.json").write_text(disk_metadata.model_dump_json())
|
||||
loader_result = (loaded_messages or [], {"session_id": session_id})
|
||||
|
||||
with (
|
||||
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=patched_loop),
|
||||
patch(
|
||||
"vibe.acp.acp_agent_loop.SessionLoader.find_session_by_id",
|
||||
return_value=session_dir,
|
||||
),
|
||||
patch(
|
||||
"vibe.acp.acp_agent_loop.SessionLoader.load_session",
|
||||
return_value=loader_result,
|
||||
),
|
||||
):
|
||||
await acp.load_session(
|
||||
cwd=str(Path.cwd()), session_id=session_id, mcp_servers=[]
|
||||
)
|
||||
spy.assert_called_once()
|
||||
return spy
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestAcpHooksLoading:
|
||||
async def test_new_session_hooks_enabled_loads_valid_hook(
|
||||
self, backend: FakeBackend, config_dir: Path
|
||||
) -> None:
|
||||
_write_config(config_dir, enable_hooks=True)
|
||||
(config_dir / "hooks.toml").write_text(
|
||||
'[[hooks]]\nname = "lint"\ntype = "post_agent_turn"\ncommand = "eslint ."\n'
|
||||
)
|
||||
|
||||
spy = await _new_session(backend)
|
||||
|
||||
result = _hook_config_from_spy(spy)
|
||||
assert result is not None
|
||||
assert len(result.hooks) == 1
|
||||
assert result.hooks[0].name == "lint"
|
||||
assert result.hooks[0].type == HookType.POST_AGENT_TURN
|
||||
assert result.hooks[0].command == "eslint ."
|
||||
assert result.hooks[0].timeout == 30.0
|
||||
assert result.issues == []
|
||||
|
||||
async def test_new_session_hooks_enabled_invalid_toml(
|
||||
self, backend: FakeBackend, config_dir: Path
|
||||
) -> None:
|
||||
_write_config(config_dir, enable_hooks=True)
|
||||
(config_dir / "hooks.toml").write_text("{{broken toml")
|
||||
|
||||
spy = await _new_session(backend)
|
||||
|
||||
result = _hook_config_from_spy(spy)
|
||||
assert result is not None
|
||||
assert result.hooks == []
|
||||
assert len(result.issues) == 1
|
||||
assert "Failed to parse" in result.issues[0].message
|
||||
|
||||
async def test_load_session_hooks_enabled_loads_valid_hook(
|
||||
self, backend: FakeBackend, config_dir: Path, tmp_path: Path
|
||||
) -> None:
|
||||
_write_config(config_dir, enable_hooks=True)
|
||||
(config_dir / "hooks.toml").write_text(
|
||||
'[[hooks]]\nname = "lint"\ntype = "post_agent_turn"\ncommand = "eslint ."\n'
|
||||
)
|
||||
|
||||
spy = await _load_session(
|
||||
backend,
|
||||
tmp_path,
|
||||
loaded_messages=[
|
||||
LLMMessage(role=Role.system, content="system"),
|
||||
LLMMessage(role=Role.user, content="hello"),
|
||||
],
|
||||
)
|
||||
|
||||
result = _hook_config_from_spy(spy)
|
||||
assert result is not None
|
||||
assert len(result.hooks) == 1
|
||||
assert result.hooks[0].name == "lint"
|
||||
assert result.hooks[0].type == HookType.POST_AGENT_TURN
|
||||
assert result.hooks[0].command == "eslint ."
|
||||
assert result.hooks[0].timeout == 30.0
|
||||
assert result.issues == []
|
||||
101
tests/acp/test_close_session.py
Normal file
101
tests/acp/test_close_session.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from acp import RequestError
|
||||
from acp.schema import TextContentBlock
|
||||
import pytest
|
||||
|
||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
|
||||
|
||||
class TestCloseSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_session_removes_session_and_closes_resources(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
session = acp_agent_loop.sessions[session_response.session_id]
|
||||
|
||||
backend_close = AsyncMock()
|
||||
telemetry_close = AsyncMock()
|
||||
cast(Any, session.agent_loop.backend).close = backend_close
|
||||
session.agent_loop.telemetry_client.aclose = telemetry_close
|
||||
|
||||
response = await acp_agent_loop.close_session(session_response.session_id)
|
||||
|
||||
assert response is not None
|
||||
assert session_response.session_id not in acp_agent_loop.sessions
|
||||
backend_close.assert_awaited_once()
|
||||
telemetry_close.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_session_cancels_active_prompt(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
session = acp_agent_loop.sessions[session_response.session_id]
|
||||
|
||||
async def wait_forever() -> None:
|
||||
await asyncio.Event().wait()
|
||||
|
||||
task = session.set_prompt_task(wait_forever())
|
||||
session.agent_loop.telemetry_client.aclose = AsyncMock()
|
||||
|
||||
await acp_agent_loop.close_session(session_response.session_id)
|
||||
|
||||
assert task.cancelled()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_session_cancels_background_tasks(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
session = acp_agent_loop.sessions[session_response.session_id]
|
||||
|
||||
background_event = asyncio.Event()
|
||||
|
||||
async def background_work() -> None:
|
||||
await background_event.wait()
|
||||
|
||||
bg_task = session.spawn(background_work())
|
||||
assert bg_task is not None
|
||||
|
||||
session.agent_loop.telemetry_client.aclose = AsyncMock()
|
||||
|
||||
await acp_agent_loop.close_session(session_response.session_id)
|
||||
|
||||
assert bg_task.cancelled()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_session_rejects_new_background_tasks(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
session = acp_agent_loop.sessions[session_response.session_id]
|
||||
session.agent_loop.telemetry_client.aclose = AsyncMock()
|
||||
|
||||
await acp_agent_loop.close_session(session_response.session_id)
|
||||
|
||||
async def noop() -> None:
|
||||
pass
|
||||
|
||||
assert session.spawn(noop()) is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_closed_session_rejects_new_prompts(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
session = acp_agent_loop.sessions[session_response.session_id]
|
||||
session.agent_loop.telemetry_client.aclose = AsyncMock()
|
||||
|
||||
await acp_agent_loop.close_session(session_response.session_id)
|
||||
|
||||
with pytest.raises(RequestError, match="Session not found"):
|
||||
await acp_agent_loop.prompt(
|
||||
prompt=[TextContentBlock(type="text", text="Hello")],
|
||||
session_id=session_response.session_id,
|
||||
)
|
||||
|
|
@ -259,6 +259,72 @@ class TestAvailableCommandsWithSkills:
|
|||
assert "hidden-skill" not in cmd_names
|
||||
|
||||
|
||||
class TestSlashCommandTelemetry:
|
||||
@pytest.mark.asyncio
|
||||
async def test_builtin_command_fires_telemetry(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict]
|
||||
) -> None:
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
telemetry_events.clear()
|
||||
|
||||
await _prompt(acp_agent_loop, session_id, "/help")
|
||||
|
||||
slash_events = [
|
||||
e for e in telemetry_events if e["event_name"] == "vibe.slash_command_used"
|
||||
]
|
||||
assert len(slash_events) == 1
|
||||
assert slash_events[0]["properties"]["command"] == "help"
|
||||
assert slash_events[0]["properties"]["command_type"] == "builtin"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skill_command_fires_telemetry(
|
||||
self,
|
||||
acp_agent_loop_with_skills: VibeAcpAgentLoop,
|
||||
skills_dir: Path,
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
create_skill(skills_dir, "my-skill", "Does something")
|
||||
session_id = await _new_session_and_clear(acp_agent_loop_with_skills)
|
||||
telemetry_events.clear()
|
||||
|
||||
await _prompt(acp_agent_loop_with_skills, session_id, "/my-skill")
|
||||
|
||||
slash_events = [
|
||||
e for e in telemetry_events if e["event_name"] == "vibe.slash_command_used"
|
||||
]
|
||||
assert len(slash_events) == 1
|
||||
assert slash_events[0]["properties"]["command"] == "my-skill"
|
||||
assert slash_events[0]["properties"]["command_type"] == "skill"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_slash_command_does_not_fire_telemetry(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict]
|
||||
) -> None:
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
telemetry_events.clear()
|
||||
|
||||
await _prompt(acp_agent_loop, session_id, "/nonexistent")
|
||||
|
||||
slash_events = [
|
||||
e for e in telemetry_events if e["event_name"] == "vibe.slash_command_used"
|
||||
]
|
||||
assert slash_events == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_regular_message_does_not_fire_telemetry(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict]
|
||||
) -> None:
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
telemetry_events.clear()
|
||||
|
||||
await _prompt(acp_agent_loop, session_id, "Hello world")
|
||||
|
||||
slash_events = [
|
||||
e for e in telemetry_events if e["event_name"] == "vibe.slash_command_used"
|
||||
]
|
||||
assert slash_events == []
|
||||
|
||||
|
||||
class TestCommandCaseInsensitivity:
|
||||
@pytest.mark.asyncio
|
||||
async def test_uppercase_command(self, acp_agent_loop: VibeAcpAgentLoop) -> None:
|
||||
|
|
|
|||
192
tests/acp/test_fork_session.py
Normal file
192
tests/acp/test_fork_session.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
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_file", arguments='{"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)
|
||||
|
|
@ -7,6 +7,8 @@ from acp.schema import (
|
|||
Implementation,
|
||||
PromptCapabilities,
|
||||
SessionCapabilities,
|
||||
SessionCloseCapabilities,
|
||||
SessionForkCapabilities,
|
||||
SessionListCapabilities,
|
||||
)
|
||||
import pytest
|
||||
|
|
@ -25,10 +27,14 @@ class TestACPInitialize:
|
|||
prompt_capabilities=PromptCapabilities(
|
||||
audio=False, embedded_context=True, image=False
|
||||
),
|
||||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
session_capabilities=SessionCapabilities(
|
||||
close=SessionCloseCapabilities(),
|
||||
list=SessionListCapabilities(),
|
||||
fork=SessionForkCapabilities(),
|
||||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.8.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -49,10 +55,14 @@ class TestACPInitialize:
|
|||
prompt_capabilities=PromptCapabilities(
|
||||
audio=False, embedded_context=True, image=False
|
||||
),
|
||||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
session_capabilities=SessionCapabilities(
|
||||
close=SessionCloseCapabilities(),
|
||||
list=SessionListCapabilities(),
|
||||
fork=SessionForkCapabilities(),
|
||||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.8.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class TestLoadSession:
|
|||
}
|
||||
|
||||
assert response.config_options is not None
|
||||
assert len(response.config_options) == 2
|
||||
assert len(response.config_options) == 3
|
||||
assert response.config_options[0].id == "mode"
|
||||
assert response.config_options[0].category == "mode"
|
||||
assert response.config_options[0].current_value == BuiltinAgentName.DEFAULT
|
||||
|
|
@ -118,6 +118,9 @@ class TestLoadSession:
|
|||
assert len(response.config_options[1].options) == 2
|
||||
model_option_values = {opt.value for opt in response.config_options[1].options}
|
||||
assert model_option_values == {"devstral-latest", "devstral-small"}
|
||||
assert response.config_options[2].id == "thinking"
|
||||
assert response.config_options[2].category == "thinking"
|
||||
assert response.config_options[2].current_value == "off"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_registers_session_with_original_id(
|
||||
|
|
@ -136,6 +139,7 @@ class TestLoadSession:
|
|||
|
||||
assert session_id in acp_agent.sessions
|
||||
assert acp_agent.sessions[session_id].id == session_id
|
||||
assert acp_agent.sessions[session_id].agent_loop.session_id == session_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_injects_messages_into_agent_loop(
|
||||
|
|
@ -375,6 +379,36 @@ class TestLoadSession:
|
|||
assert agent_updates[0].update.content.text == "First response"
|
||||
assert agent_updates[1].update.content.text == "Second response"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_restores_agent_loop_session_identity(
|
||||
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 = "restore-id-12345678"
|
||||
parent_session_id = "parent-id-87654321"
|
||||
cwd = str(Path.cwd())
|
||||
session_dir = create_test_session(
|
||||
temp_session_dir, session_id, cwd, parent_session_id=parent_session_id
|
||||
)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
agent_loop = acp_agent.sessions[session_id].agent_loop
|
||||
|
||||
assert agent_loop.session_id == session_id
|
||||
assert agent_loop.parent_session_id == parent_session_id
|
||||
assert agent_loop.session_logger.session_id == session_id
|
||||
assert agent_loop.session_logger.session_dir == session_dir
|
||||
assert agent_loop.session_logger.session_metadata is not None
|
||||
assert (
|
||||
agent_loop.session_logger.session_metadata.parent_session_id
|
||||
== parent_session_id
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_replay_user_message_has_message_id(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class TestACPNewSession:
|
|||
|
||||
# Check config_options
|
||||
assert session_response.config_options is not None
|
||||
assert len(session_response.config_options) == 2
|
||||
assert len(session_response.config_options) == 3
|
||||
|
||||
# Mode config option
|
||||
mode_config = session_response.config_options[0]
|
||||
|
|
@ -125,6 +125,13 @@ class TestACPNewSession:
|
|||
model_option_values = {opt.value for opt in model_config.options}
|
||||
assert model_option_values == {"devstral-latest", "devstral-small"}
|
||||
|
||||
# Thinking config option
|
||||
thinking_config = session_response.config_options[2]
|
||||
assert thinking_config.id == "thinking"
|
||||
assert thinking_config.category == "thinking"
|
||||
assert thinking_config.current_value == "off"
|
||||
assert len(thinking_config.options) == 5
|
||||
|
||||
@pytest.mark.skip(reason="TODO: Fix this test")
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_session_preserves_model_after_set_model(
|
||||
|
|
|
|||
155
tests/acp/test_session.py
Normal file
155
tests/acp/test_session.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.acp.session import AcpSessionLoop
|
||||
|
||||
|
||||
def _make_session() -> AcpSessionLoop:
|
||||
return AcpSessionLoop(
|
||||
id="test-session", agent_loop=MagicMock(), command_registry=MagicMock()
|
||||
)
|
||||
|
||||
|
||||
class TestSpawn:
|
||||
@pytest.mark.asyncio
|
||||
async def test_spawn_creates_task(self) -> None:
|
||||
session = _make_session()
|
||||
ran = asyncio.Event()
|
||||
|
||||
async def work() -> None:
|
||||
ran.set()
|
||||
|
||||
task = session.spawn(work())
|
||||
|
||||
assert task is not None
|
||||
await task
|
||||
assert ran.is_set()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_spawn_returns_none_after_close(self) -> None:
|
||||
session = _make_session()
|
||||
await session.close()
|
||||
|
||||
async def noop() -> None:
|
||||
pass
|
||||
|
||||
assert session.spawn(noop()) is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_spawn_tracks_multiple_tasks(self) -> None:
|
||||
session = _make_session()
|
||||
gate = asyncio.Event()
|
||||
|
||||
async def wait_for_gate() -> None:
|
||||
await gate.wait()
|
||||
|
||||
t1 = session.spawn(wait_for_gate())
|
||||
t2 = session.spawn(wait_for_gate())
|
||||
|
||||
assert t1 is not None
|
||||
assert t2 is not None
|
||||
assert not t1.done()
|
||||
assert not t2.done()
|
||||
|
||||
gate.set()
|
||||
await asyncio.gather(t1, t2)
|
||||
|
||||
|
||||
class TestPromptTask:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_prompt_task_tracks_task(self) -> None:
|
||||
session = _make_session()
|
||||
|
||||
async def work() -> None:
|
||||
pass
|
||||
|
||||
task = session.set_prompt_task(work())
|
||||
assert session.prompt_task is task
|
||||
await task
|
||||
assert session.prompt_task is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_prompt_cancels_active_task(self) -> None:
|
||||
session = _make_session()
|
||||
|
||||
async def hang() -> None:
|
||||
await asyncio.Event().wait()
|
||||
|
||||
task = session.set_prompt_task(hang())
|
||||
await session.cancel_prompt()
|
||||
assert task.cancelled()
|
||||
assert session.prompt_task is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_prompt_is_noop_without_task(self) -> None:
|
||||
session = _make_session()
|
||||
await session.cancel_prompt()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_prompt_does_not_cancel_background_tasks(self) -> None:
|
||||
session = _make_session()
|
||||
gate = asyncio.Event()
|
||||
|
||||
async def bg() -> None:
|
||||
await gate.wait()
|
||||
|
||||
bg_task = session.spawn(bg())
|
||||
assert bg_task is not None
|
||||
|
||||
async def hang() -> None:
|
||||
await asyncio.Event().wait()
|
||||
|
||||
session.set_prompt_task(hang())
|
||||
await session.cancel_prompt()
|
||||
|
||||
assert not bg_task.cancelled()
|
||||
gate.set()
|
||||
await bg_task
|
||||
|
||||
|
||||
class TestClose:
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_cancels_all_tasks(self) -> None:
|
||||
session = _make_session()
|
||||
|
||||
async def hang() -> None:
|
||||
await asyncio.Event().wait()
|
||||
|
||||
bg = session.spawn(hang())
|
||||
prompt = session.set_prompt_task(hang())
|
||||
|
||||
assert bg is not None
|
||||
|
||||
await session.close()
|
||||
|
||||
assert bg.cancelled()
|
||||
assert prompt.cancelled()
|
||||
assert session.prompt_task is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_is_idempotent(self) -> None:
|
||||
session = _make_session()
|
||||
await session.close()
|
||||
await session.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_waits_for_task_cleanup(self) -> None:
|
||||
session = _make_session()
|
||||
cleanup_ran = asyncio.Event()
|
||||
|
||||
async def task_with_cleanup() -> None:
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
except asyncio.CancelledError:
|
||||
cleanup_ran.set()
|
||||
raise
|
||||
|
||||
session.spawn(task_with_cleanup())
|
||||
await asyncio.sleep(0) # let the task start
|
||||
await session.close()
|
||||
|
||||
assert cleanup_ran.is_set()
|
||||
|
|
@ -75,11 +75,11 @@ class TestACPSetConfigOptionMode:
|
|||
|
||||
assert response is not None
|
||||
assert response.config_options is not None
|
||||
assert len(response.config_options) == 2
|
||||
assert len(response.config_options) == 3
|
||||
assert (
|
||||
acp_session.agent_loop.agent_profile.name == BuiltinAgentName.AUTO_APPROVE
|
||||
)
|
||||
assert acp_session.agent_loop.auto_approve is True
|
||||
assert acp_session.agent_loop.bypass_tool_permissions is True
|
||||
|
||||
# Verify config_options reflect the new state
|
||||
mode_config = response.config_options[0]
|
||||
|
|
@ -126,10 +126,10 @@ class TestACPSetConfigOptionMode:
|
|||
|
||||
assert response is not None
|
||||
assert response.config_options is not None
|
||||
assert len(response.config_options) == 2
|
||||
assert len(response.config_options) == 3
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.CHAT
|
||||
assert (
|
||||
acp_session.agent_loop.auto_approve is True
|
||||
acp_session.agent_loop.bypass_tool_permissions is True
|
||||
) # Chat mode auto-approves read-only tools
|
||||
|
||||
mode_config = response.config_options[0]
|
||||
|
|
@ -200,7 +200,7 @@ class TestACPSetConfigOptionModel:
|
|||
|
||||
assert response is not None
|
||||
assert response.config_options is not None
|
||||
assert len(response.config_options) == 2
|
||||
assert len(response.config_options) == 3
|
||||
assert acp_session.agent_loop.config.active_model == "devstral-small"
|
||||
|
||||
# Verify config_options reflect the new state
|
||||
|
|
@ -315,3 +315,87 @@ class TestACPSetConfigOptionInvalidConfigId:
|
|||
)
|
||||
|
||||
assert response is None
|
||||
|
||||
|
||||
class TestACPSetConfigOptionThinking:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_thinking_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
|
||||
assert acp_session.agent_loop.config.get_active_model().thinking == "off"
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="thinking", value="high"
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.config_options is not None
|
||||
assert len(response.config_options) == 3
|
||||
assert acp_session.agent_loop.config.get_active_model().thinking == "high"
|
||||
|
||||
thinking_config = response.config_options[2]
|
||||
assert thinking_config.id == "thinking"
|
||||
assert thinking_config.current_value == "high"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_thinking_all_levels(
|
||||
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
|
||||
|
||||
for level in ["low", "medium", "high", "max", "off"]:
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="thinking", value=level
|
||||
)
|
||||
assert response is not None
|
||||
assert acp_session.agent_loop.config.get_active_model().thinking == level
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_thinking_invalid_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
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="thinking", value="ultra"
|
||||
)
|
||||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop.config.get_active_model().thinking == "off"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_config_option_thinking_empty_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
|
||||
|
||||
response = await acp_agent_loop.set_config_option(
|
||||
session_id=session_id, config_id="thinking", value=""
|
||||
)
|
||||
|
||||
assert response is None
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class TestACPSetMode:
|
|||
|
||||
assert response is not None
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.DEFAULT
|
||||
assert acp_session.agent_loop.auto_approve is False
|
||||
assert acp_session.agent_loop.bypass_tool_permissions is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_to_auto_approve(
|
||||
|
|
@ -44,7 +44,7 @@ class TestACPSetMode:
|
|||
assert acp_session is not None
|
||||
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.DEFAULT
|
||||
assert acp_session.agent_loop.auto_approve is False
|
||||
assert acp_session.agent_loop.bypass_tool_permissions is False
|
||||
|
||||
response = await acp_agent_loop.set_session_mode(
|
||||
session_id=session_id, mode_id=BuiltinAgentName.AUTO_APPROVE
|
||||
|
|
@ -54,7 +54,7 @@ class TestACPSetMode:
|
|||
assert (
|
||||
acp_session.agent_loop.agent_profile.name == BuiltinAgentName.AUTO_APPROVE
|
||||
)
|
||||
assert acp_session.agent_loop.auto_approve is True
|
||||
assert acp_session.agent_loop.bypass_tool_permissions is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_to_plan(self, acp_agent_loop: VibeAcpAgentLoop) -> None:
|
||||
|
|
@ -76,7 +76,7 @@ class TestACPSetMode:
|
|||
assert response is not None
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.PLAN
|
||||
assert (
|
||||
acp_session.agent_loop.auto_approve is False
|
||||
acp_session.agent_loop.bypass_tool_permissions is False
|
||||
) # Plan mode uses per-tool allowlists, not global auto-approve
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -103,7 +103,7 @@ class TestACPSetMode:
|
|||
acp_session.agent_loop.agent_profile.name == BuiltinAgentName.ACCEPT_EDITS
|
||||
)
|
||||
assert (
|
||||
acp_session.agent_loop.auto_approve is False
|
||||
acp_session.agent_loop.bypass_tool_permissions is False
|
||||
) # Accept Edits mode doesn't auto-approve all
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -126,7 +126,7 @@ class TestACPSetMode:
|
|||
assert response is not None
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.CHAT
|
||||
assert (
|
||||
acp_session.agent_loop.auto_approve is True
|
||||
acp_session.agent_loop.bypass_tool_permissions is True
|
||||
) # Chat mode auto-approves read-only tools
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -143,7 +143,7 @@ class TestACPSetMode:
|
|||
assert acp_session is not None
|
||||
|
||||
initial_agent = acp_session.agent_loop.agent_profile.name
|
||||
initial_auto_approve = acp_session.agent_loop.auto_approve
|
||||
initial_bypass = acp_session.agent_loop.bypass_tool_permissions
|
||||
|
||||
response = await acp_agent_loop.set_session_mode(
|
||||
session_id=session_id, mode_id="invalid-mode"
|
||||
|
|
@ -151,7 +151,7 @@ class TestACPSetMode:
|
|||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop.agent_profile.name == initial_agent
|
||||
assert acp_session.agent_loop.auto_approve == initial_auto_approve
|
||||
assert acp_session.agent_loop.bypass_tool_permissions == initial_bypass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_to_same_mode(
|
||||
|
|
@ -174,7 +174,7 @@ class TestACPSetMode:
|
|||
|
||||
assert response is not None
|
||||
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.DEFAULT
|
||||
assert acp_session.agent_loop.auto_approve is False
|
||||
assert acp_session.agent_loop.bypass_tool_permissions is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_with_empty_string(
|
||||
|
|
@ -190,7 +190,7 @@ class TestACPSetMode:
|
|||
assert acp_session is not None
|
||||
|
||||
initial_agent = acp_session.agent_loop.agent_profile.name
|
||||
initial_auto_approve = acp_session.agent_loop.auto_approve
|
||||
initial_bypass = acp_session.agent_loop.bypass_tool_permissions
|
||||
|
||||
response = await acp_agent_loop.set_session_mode(
|
||||
session_id=session_id, mode_id=""
|
||||
|
|
@ -198,4 +198,4 @@ class TestACPSetMode:
|
|||
|
||||
assert response is None
|
||||
assert acp_session.agent_loop.agent_profile.name == initial_agent
|
||||
assert acp_session.agent_loop.auto_approve == initial_auto_approve
|
||||
assert acp_session.agent_loop.bypass_tool_permissions == initial_bypass
|
||||
|
|
|
|||
53
tests/acp/test_telemetry_notification.py
Normal file
53
tests/acp/test_telemetry_notification.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
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.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
from vibe.acp.exceptions import InvalidRequestError
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_loop(backend) -> VibeAcpAgentLoop:
|
||||
class PatchedAgentLoop(AgentLoop):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **{**kwargs, "backend": backend})
|
||||
|
||||
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgentLoop).start()
|
||||
return _create_acp_agent()
|
||||
|
||||
|
||||
class TestTelemetryNotification:
|
||||
@pytest.mark.asyncio
|
||||
async def test_ignores_unknown_event_gracefully(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
telemetry_events: list[dict[str, object]],
|
||||
) -> None:
|
||||
session = await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
telemetry_events.clear()
|
||||
|
||||
await acp_agent_loop.ext_notification(
|
||||
"telemetry/send",
|
||||
{
|
||||
"event": "vibe.unsupported_event",
|
||||
"session_id": session.session_id,
|
||||
"properties": {"context_type": "file"},
|
||||
},
|
||||
)
|
||||
|
||||
assert telemetry_events == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_on_invalid_params(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent_loop.ext_notification(
|
||||
"telemetry/send",
|
||||
{"event": "vibe.some_event"}, # missing session_id
|
||||
)
|
||||
|
|
@ -212,6 +212,9 @@ class TestLoadSessionUsageUpdate:
|
|||
"session_id": session_id,
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": "2024-01-01T12:05:00Z",
|
||||
"git_commit": None,
|
||||
"git_branch": None,
|
||||
"username": "test-user",
|
||||
"environment": {"working_directory": cwd},
|
||||
}
|
||||
with (session_folder / "meta.json").open("w") as f:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue