from __future__ import annotations import asyncio from collections.abc import AsyncGenerator from typing import Any, cast import pytest from vibe.core.agent_loop import AgentLoop, TeleportError 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 _set_teleport_service(agent_loop: AgentLoop, service: object) -> None: agent_loop._teleport_service = cast(TeleportService, service) class TestTeleportAgentLoopTelemetry: @pytest.mark.asyncio async def test_teleport_to_vibe_code_sends_completed_success( self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]] ) -> None: class FakeTeleportService: async def __aenter__(self) -> FakeTeleportService: return self async def __aexit__(self, *_args: object) -> None: return None async def execute( self, *_args: object, **_kwargs: object ) -> AsyncGenerator[object, object]: yield TeleportCheckingGitEvent() yield TeleportPushRequiredEvent() yield TeleportPushingEvent() yield TeleportStartingWorkflowEvent() yield TeleportCompleteEvent(url="https://chat.example.com/123") agent_loop.messages.append(LLMMessage(role=Role.user, content="hello")) _set_teleport_service(agent_loop, FakeTeleportService()) gen = agent_loop.teleport_to_vibe_code(None) response = None events = [] while True: try: event = await gen.asend(response) except StopAsyncIteration: break events.append(event) response = ( TeleportPushResponseEvent(approved=True) if isinstance(event, TeleportPushRequiredEvent) else None ) assert isinstance(events[-1], TeleportCompleteEvent) assert telemetry_events[-1]["event_name"] == "vibe.teleport_completed" assert telemetry_events[-1]["properties"] == { "push_required": True, "nb_session_messages": 1, "session_id": agent_loop.session_id, } @pytest.mark.asyncio async def test_teleport_to_vibe_code_sends_failed_stage( self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]] ) -> None: class FakeTeleportService: async def __aenter__(self) -> FakeTeleportService: return self async def __aexit__(self, *_args: object) -> None: return None async def execute( self, *_args: object, **_kwargs: object ) -> AsyncGenerator[object, object]: yield TeleportCheckingGitEvent() yield TeleportStartingWorkflowEvent() raise ServiceTeleportError( "Workflow api-key-123 could not be started.", telemetry_details={"http_status_code": 502}, ) agent_loop.messages.append(LLMMessage(role=Role.user, content="hello")) _set_teleport_service(agent_loop, FakeTeleportService()) gen = agent_loop.teleport_to_vibe_code(None) with pytest.raises(TeleportError, match="api-key-123"): async for _ in gen: pass assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed" assert telemetry_events[-1]["properties"] == { "stage": "workflow_start", "error_class": "ServiceTeleportError", "push_required": False, "nb_session_messages": 1, "http_status_code": 502, "session_id": agent_loop.session_id, } assert "api-key-123" not in str(telemetry_events[-1]["properties"]) @pytest.mark.asyncio async def test_teleport_to_vibe_code_sends_failed_cancelled( self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]] ) -> None: class FakeTeleportService: async def __aenter__(self) -> FakeTeleportService: return self async def __aexit__(self, *_args: object) -> None: return None async def execute( self, *_args: object, **_kwargs: object ) -> AsyncGenerator[object, object]: yield TeleportCheckingGitEvent() response = yield TeleportPushRequiredEvent() if ( not isinstance(response, TeleportPushResponseEvent) or not response.approved ): raise ServiceTeleportError( "Teleport cancelled: changes not pushed." ) agent_loop.messages.append(LLMMessage(role=Role.user, content="hello")) _set_teleport_service(agent_loop, FakeTeleportService()) gen = agent_loop.teleport_to_vibe_code(None) assert isinstance(await gen.asend(None), TeleportCheckingGitEvent) assert isinstance(await gen.asend(None), TeleportPushRequiredEvent) with pytest.raises(TeleportError, match="Teleport cancelled"): await gen.asend(TeleportPushResponseEvent(approved=False)) assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed" assert telemetry_events[-1]["properties"] == { "stage": "cancelled", "error_class": "ServiceTeleportError", "push_required": True, "nb_session_messages": 1, "session_id": agent_loop.session_id, } @pytest.mark.asyncio async def test_teleport_to_vibe_code_sends_failed_when_task_cancelled( self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]] ) -> None: class FakeTeleportService: async def __aenter__(self) -> FakeTeleportService: return self async def __aexit__(self, *_args: object) -> None: return None async def execute( self, *_args: object, **_kwargs: object ) -> AsyncGenerator[object, object]: yield TeleportCheckingGitEvent() raise asyncio.CancelledError agent_loop.messages.append(LLMMessage(role=Role.user, content="hello")) _set_teleport_service(agent_loop, FakeTeleportService()) gen = agent_loop.teleport_to_vibe_code(None) assert isinstance(await gen.asend(None), TeleportCheckingGitEvent) with pytest.raises(asyncio.CancelledError): await gen.asend(None) assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed" assert telemetry_events[-1]["properties"] == { "stage": "cancelled", "error_class": "CancelledError", "push_required": False, "nb_session_messages": 1, "session_id": agent_loop.session_id, } @pytest.mark.asyncio async def test_teleport_to_vibe_code_sends_failed_when_consumer_closes_generator( self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]] ) -> None: class FakeTeleportService: async def __aenter__(self) -> FakeTeleportService: return self async def __aexit__(self, *_args: object) -> None: return None async def execute( self, *_args: object, **_kwargs: object ) -> AsyncGenerator[object, object]: yield TeleportCheckingGitEvent() yield TeleportPushRequiredEvent() agent_loop.messages.append(LLMMessage(role=Role.user, content="hello")) _set_teleport_service(agent_loop, FakeTeleportService()) gen = agent_loop.teleport_to_vibe_code(None) assert isinstance(await gen.asend(None), TeleportCheckingGitEvent) await gen.aclose() assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed" assert telemetry_events[-1]["properties"] == { "stage": "cancelled", "error_class": "CancelledError", "push_required": False, "nb_session_messages": 1, "session_id": agent_loop.session_id, }