v2.9.5 (#676)
Co-authored-by: Clément Drouin <clement.drouin@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: allansimon-mistral <allan.simon@ext.mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
4972dd5694
commit
b23f49e5f4
74 changed files with 2342 additions and 240 deletions
|
|
@ -104,3 +104,19 @@ class TestAgentManager:
|
|||
)
|
||||
manager = AgentManager(lambda: config, initial_agent="plan")
|
||||
assert manager.active_profile.name == "plan"
|
||||
|
||||
def test_initial_agent_raises_when_agent_is_disabled(self) -> None:
|
||||
config = build_test_vibe_config(
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
disabled_agents=["plan"],
|
||||
)
|
||||
with pytest.raises(ValueError, match="not available"):
|
||||
AgentManager(lambda: config, initial_agent="plan")
|
||||
|
||||
def test_initial_agent_raises_when_agent_does_not_exist(self) -> None:
|
||||
config = build_test_vibe_config(
|
||||
include_project_context=False, include_prompt_detail=False
|
||||
)
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
AgentManager(lambda: config, initial_agent="nonexistent-agent")
|
||||
|
|
|
|||
304
tests/core/test_loop.py
Normal file
304
tests/core/test_loop.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.loop import (
|
||||
MAX_LOOPS_PER_SESSION,
|
||||
MIN_INTERVAL_SECONDS,
|
||||
LoopError,
|
||||
LoopErrorResult,
|
||||
LoopListResult,
|
||||
LoopManager,
|
||||
LoopOkResult,
|
||||
ScheduledLoop,
|
||||
format_duration,
|
||||
parse_interval,
|
||||
)
|
||||
from vibe.core.session.session_logger import SessionLogger
|
||||
|
||||
|
||||
class FakeMetadata:
|
||||
def __init__(self) -> None:
|
||||
self.loops: list[ScheduledLoop] = []
|
||||
|
||||
|
||||
class FakeSessionLogger:
|
||||
def __init__(self) -> None:
|
||||
self.session_metadata = FakeMetadata()
|
||||
self.persisted: list[list[ScheduledLoop]] = []
|
||||
|
||||
async def persist_loops(self) -> None:
|
||||
self.persisted.append([*self.session_metadata.loops])
|
||||
|
||||
|
||||
class RaisingSessionLogger:
|
||||
def __init__(self) -> None:
|
||||
self.session_metadata = FakeMetadata()
|
||||
|
||||
async def persist_loops(self) -> None:
|
||||
raise RuntimeError("disk on fire")
|
||||
|
||||
|
||||
def _build_manager() -> tuple[LoopManager, FakeSessionLogger]:
|
||||
fake = FakeSessionLogger()
|
||||
return LoopManager(cast(SessionLogger, fake)), fake
|
||||
|
||||
|
||||
class TestParseInterval:
|
||||
def test_parses_supported_units(self) -> None:
|
||||
assert parse_interval("30s") == 30
|
||||
assert parse_interval("5m") == 300
|
||||
assert parse_interval("2h") == 7200
|
||||
assert parse_interval("1d") == 86400
|
||||
|
||||
def test_parses_case_insensitively(self) -> None:
|
||||
assert parse_interval("30S") == 30
|
||||
assert parse_interval("2H") == 7200
|
||||
|
||||
def test_strips_whitespace(self) -> None:
|
||||
assert parse_interval(" 30s ") == 30
|
||||
|
||||
@pytest.mark.parametrize("bad", ["", "5", "5x", "-5m", "5 m", "1.5m", "abc"])
|
||||
def test_rejects_invalid_inputs(self, bad: str) -> None:
|
||||
with pytest.raises(LoopError):
|
||||
parse_interval(bad)
|
||||
|
||||
def test_rejects_below_minimum(self) -> None:
|
||||
with pytest.raises(LoopError, match=f"at least {MIN_INTERVAL_SECONDS}"):
|
||||
parse_interval("29s")
|
||||
|
||||
|
||||
class TestFormatInterval:
|
||||
@pytest.mark.parametrize("text", ["30s", "5m", "2h", "1d"])
|
||||
def test_round_trip_with_parse(self, text: str) -> None:
|
||||
assert format_duration(parse_interval(text)) == text
|
||||
|
||||
def test_formats_non_round_values(self) -> None:
|
||||
assert format_duration(90) == "1m30s"
|
||||
assert format_duration(180) == "3m"
|
||||
assert format_duration(7200) == "2h"
|
||||
assert format_duration(259200) == "3d"
|
||||
|
||||
def test_formats_short_version(self) -> None:
|
||||
assert format_duration(90, short=True) == "1m"
|
||||
assert format_duration(180, short=True) == "3m"
|
||||
assert format_duration(190, short=True) == "3m"
|
||||
assert format_duration(8000, short=True) == "2h"
|
||||
assert format_duration(260000, short=True) == "3d"
|
||||
|
||||
|
||||
class TestLoopManagerHandleCommand:
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_returns_loops_list(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("")
|
||||
assert isinstance(result, LoopListResult)
|
||||
assert result.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("verb", ["list", "ls"])
|
||||
async def test_list_alias_returns_loops_list(self, verb: str) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command(verb)
|
||||
assert isinstance(result, LoopListResult)
|
||||
assert result.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_success_persists_and_returns_id(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("1m hello world")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert "every 1m" in result.message
|
||||
assert "hello world" in result.message
|
||||
assert len(manager.loops) == 1
|
||||
loop = manager.loops[0]
|
||||
assert loop.interval_seconds == 60
|
||||
assert loop.prompt == "hello world"
|
||||
assert result.message.count(loop.id) >= 1
|
||||
assert len(fake.persisted) == 1
|
||||
assert fake.persisted[0][0].id == loop.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_bad_interval_returns_error_no_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("nope hello")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "Invalid interval" in result.message
|
||||
assert "Usage:" not in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_empty_prompt_returns_error_no_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("30s ")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "Missing prompt" in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_prompt_starting_with_slash_returns_error(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("30s /config")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "cannot start with '/'" in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_over_limit_returns_error(self) -> None:
|
||||
manager, _ = _build_manager()
|
||||
for i in range(MAX_LOOPS_PER_SESSION):
|
||||
result = await manager.handle_command(f"30s prompt {i}")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
result = await manager.handle_command("30s overflow")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "limit" in result.message.lower()
|
||||
assert len(manager.loops) == MAX_LOOPS_PER_SESSION
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_missing_target_is_error_no_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("cancel")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "Missing loop id" in result.message
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_unknown_id_is_error_no_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
fake.persisted.clear()
|
||||
result = await manager.handle_command("cancel deadbeef")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "deadbeef" in result.message
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_known_id_persists(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
loop_id = manager.loops[0].id
|
||||
fake.persisted.clear()
|
||||
result = await manager.handle_command(f"cancel {loop_id}")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert loop_id in result.message
|
||||
assert "ping" in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == [[]]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_all_persists(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s a")
|
||||
await manager.handle_command("30s b")
|
||||
fake.persisted.clear()
|
||||
result = await manager.handle_command("cancel all")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert "2 scheduled loop(s)" in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == [[]]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("verb", ["rm", "stop", "delete"])
|
||||
async def test_cancel_alias_verbs_work(self, verb: str) -> None:
|
||||
manager, _ = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
loop_id = manager.loops[0].id
|
||||
result = await manager.handle_command(f"{verb} {loop_id}")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert manager.loops == []
|
||||
|
||||
|
||||
class TestLoopManagerPopDue:
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_none_when_empty(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
assert await manager.pop_due() is None
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_none_when_nothing_due(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
fake.persisted.clear()
|
||||
result = await manager.pop_due(now=0.0)
|
||||
assert result is None
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_due_loop_advances_and_persists(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
fake.persisted.clear()
|
||||
loop = manager.loops[0]
|
||||
original_next = loop.next_fire_at
|
||||
due = await manager.pop_due(now=original_next + 100.0)
|
||||
assert due is not None
|
||||
assert due.id == loop.id
|
||||
assert manager.loops[0].next_fire_at == original_next + 100.0 + 30.0
|
||||
assert len(fake.persisted) == 1
|
||||
|
||||
|
||||
class TestLoopManagerNextDueIn:
|
||||
def test_inf_when_empty(self) -> None:
|
||||
manager, _ = _build_manager()
|
||||
assert manager.next_due_in() == math.inf
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_correct_delta_when_populated(self) -> None:
|
||||
manager, _ = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
loop = manager.loops[0]
|
||||
delta = manager.next_due_in(now=loop.next_fire_at - 7.0)
|
||||
assert delta == pytest.approx(7.0)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_zero_when_overdue(self) -> None:
|
||||
manager, _ = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
loop = manager.loops[0]
|
||||
delta = manager.next_due_in(now=loop.next_fire_at + 100.0)
|
||||
assert delta == 0.0
|
||||
|
||||
|
||||
class TestLoopManagerRestore:
|
||||
def test_replaces_in_memory_list_without_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
loops = [
|
||||
ScheduledLoop(
|
||||
id="aabbccdd",
|
||||
interval_seconds=30,
|
||||
prompt="x",
|
||||
next_fire_at=1.0,
|
||||
created_at=0.0,
|
||||
),
|
||||
ScheduledLoop(
|
||||
id="11223344",
|
||||
interval_seconds=60,
|
||||
prompt="y",
|
||||
next_fire_at=2.0,
|
||||
created_at=0.0,
|
||||
),
|
||||
]
|
||||
manager.restore(loops)
|
||||
assert [loop.id for loop in manager.loops] == ["aabbccdd", "11223344"]
|
||||
assert fake.persisted == []
|
||||
|
||||
|
||||
class TestLoopManagerPersisterErrors:
|
||||
@pytest.mark.asyncio
|
||||
async def test_persister_exception_does_not_propagate(self) -> None:
|
||||
manager = LoopManager(cast(SessionLogger, RaisingSessionLogger()))
|
||||
result = await manager.handle_command("30s ping")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert len(manager.loops) == 1
|
||||
loop = manager.loops[0]
|
||||
due = await manager.pop_due(now=loop.next_fire_at + 1.0)
|
||||
assert due is not None
|
||||
|
|
@ -362,6 +362,48 @@ class TestTelemetryClient:
|
|||
assert telemetry_events[1]["properties"]["command"] == "my_skill"
|
||||
assert telemetry_events[1]["properties"]["command_type"] == "skill"
|
||||
|
||||
def test_send_teleport_completed_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_teleport_completed(
|
||||
push_required=True, github_auth_required=False, nb_session_messages=4
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe.teleport_completed"
|
||||
assert telemetry_events[0]["properties"] == {
|
||||
"push_required": True,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 4,
|
||||
}
|
||||
|
||||
def test_send_teleport_failed_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_teleport_failed(
|
||||
stage="push",
|
||||
error_class="ServiceTeleportError",
|
||||
push_required=True,
|
||||
github_auth_required=False,
|
||||
nb_session_messages=4,
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe.teleport_failed"
|
||||
assert telemetry_events[0]["properties"] == {
|
||||
"stage": "push",
|
||||
"error_class": "ServiceTeleportError",
|
||||
"push_required": True,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 4,
|
||||
}
|
||||
|
||||
def test_send_new_session_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
|
|
@ -393,6 +435,55 @@ class TestTelemetryClient:
|
|||
assert properties["terminal_emulator"] == "vscode"
|
||||
assert "version" in properties
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_session_closed_payload(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
TelemetryClient, "send_telemetry_event", _original_send_telemetry_event
|
||||
)
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
env_key = config.get_active_provider().api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
client = TelemetryClient(
|
||||
config_getter=lambda: config,
|
||||
session_id_getter=lambda: "current-session",
|
||||
parent_session_id_getter=lambda: "current-parent-session",
|
||||
entrypoint_metadata_getter=lambda: EntrypointMetadata(
|
||||
agent_entrypoint="cli",
|
||||
agent_version="1.0.0",
|
||||
client_name="vibe_cli",
|
||||
client_version="1.0.0",
|
||||
),
|
||||
)
|
||||
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
|
||||
client._client = MagicMock()
|
||||
client._client.post = mock_post
|
||||
client._client.aclose = AsyncMock()
|
||||
|
||||
client.send_session_closed()
|
||||
await client.aclose()
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
"https://api.mistral.ai/v1/datalake/events",
|
||||
json={
|
||||
"event": "vibe.session_closed",
|
||||
"properties": {
|
||||
"agent_entrypoint": "cli",
|
||||
"agent_version": "1.0.0",
|
||||
"client_name": "vibe_cli",
|
||||
"client_version": "1.0.0",
|
||||
"session_id": "current-session",
|
||||
"parent_session_id": "current-parent-session",
|
||||
},
|
||||
},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer sk-test",
|
||||
"User-Agent": get_user_agent(Backend.MISTRAL),
|
||||
},
|
||||
)
|
||||
|
||||
def test_build_base_metadata_includes_entrypoint_and_session(self) -> None:
|
||||
metadata = build_base_metadata(
|
||||
entrypoint_metadata=EntrypointMetadata(
|
||||
|
|
|
|||
231
tests/core/test_teleport_telemetry.py
Normal file
231
tests/core/test_teleport_telemetry.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
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 (
|
||||
TeleportAuthRequiredEvent,
|
||||
TeleportCheckingGitEvent,
|
||||
TeleportCompleteEvent,
|
||||
TeleportFetchingUrlEvent,
|
||||
TeleportPushingEvent,
|
||||
TeleportPushRequiredEvent,
|
||||
TeleportPushResponseEvent,
|
||||
TeleportStartingWorkflowEvent,
|
||||
TeleportWaitingForGitHubEvent,
|
||||
)
|
||||
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 TeleportWaitingForGitHubEvent()
|
||||
yield TeleportAuthRequiredEvent(oauth_url="https://github.com/auth")
|
||||
yield TeleportFetchingUrlEvent()
|
||||
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,
|
||||
"github_auth_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.")
|
||||
|
||||
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,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 1,
|
||||
"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,
|
||||
"github_auth_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_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,
|
||||
"github_auth_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,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 1,
|
||||
"session_id": agent_loop.session_id,
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from vibe.core.utils import get_server_url_from_api_base
|
||||
from vibe.core.utils import compact_reduction_display, get_server_url_from_api_base
|
||||
import vibe.core.utils.io as io_utils
|
||||
from vibe.core.utils.io import decode_safe, read_safe, read_safe_async
|
||||
|
||||
|
|
@ -23,6 +23,24 @@ def test_get_server_url_from_api_base(api_base, expected):
|
|||
assert get_server_url_from_api_base(api_base) == expected
|
||||
|
||||
|
||||
class TestCompactReductionDisplay:
|
||||
def test_includes_session_ids_when_available(self) -> None:
|
||||
assert compact_reduction_display(
|
||||
177_017,
|
||||
23_263,
|
||||
old_session_id="11111111-1111-1111-1111-111111111111",
|
||||
new_session_id="22222222-2222-2222-2222-222222222222",
|
||||
) == (
|
||||
"Compaction complete: 177,017 → 23,263 tokens (-87.%)\n"
|
||||
"session: 11111111 (before compaction) → 22222222 (after compaction)"
|
||||
)
|
||||
|
||||
def test_returns_base_message_without_session_ids(self) -> None:
|
||||
assert compact_reduction_display(177_017, 23_263) == (
|
||||
"Compaction complete: 177,017 → 23,263 tokens (-87.%)"
|
||||
)
|
||||
|
||||
|
||||
class TestReadSafe:
|
||||
def test_reads_utf8(self, tmp_path: Path) -> None:
|
||||
f = tmp_path / "hello.txt"
|
||||
|
|
|
|||
|
|
@ -60,7 +60,62 @@ class TestReadFileExecution:
|
|||
|
||||
assert result.content == "line 50\nline 51\n"
|
||||
assert result.lines_read == 2
|
||||
assert result.was_truncated
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_marks_truncated_when_max_read_bytes_exceeded(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "big.txt"
|
||||
test_file.write_text(
|
||||
"".join(f"line {i}\n" for i in range(100)), encoding="utf-8"
|
||||
)
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=20),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(tool.run(ReadFileArgs(path=str(test_file))))
|
||||
|
||||
assert result.was_truncated
|
||||
assert result.lines_read > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_not_truncated_when_eof_reached(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "small.txt"
|
||||
test_file.write_text("a\nb\nc\n", encoding="utf-8")
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=1024),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(tool.run(ReadFileArgs(path=str(test_file))))
|
||||
|
||||
assert not result.was_truncated
|
||||
assert result.lines_read == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_not_truncated_when_limit_matches_remaining_lines(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "exact.txt"
|
||||
test_file.write_text("a\nb\nc\n", encoding="utf-8")
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=1024),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(
|
||||
tool.run(ReadFileArgs(path=str(test_file), limit=10))
|
||||
)
|
||||
|
||||
assert not result.was_truncated
|
||||
assert result.lines_read == 3
|
||||
|
||||
|
||||
class TestGetResultExtra:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue