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:
Clément Sirieix 2026-05-07 13:55:56 +02:00 committed by GitHub
parent 4972dd5694
commit b23f49e5f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 2342 additions and 240 deletions

View file

@ -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
View 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

View file

@ -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(

View 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,
}

View file

@ -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"

View file

@ -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: