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: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@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: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
171 lines
5.5 KiB
Python
171 lines
5.5 KiB
Python
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 == 60.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 == 60.0
|
|
assert result.issues == []
|