Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Corentin André <corentin.andre@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
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: Peter Evers <pevers90@gmail.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: MichisGitIsKing <MichisGitIsKing@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-05-19 11:56:25 +02:00 committed by GitHub
parent 626f905186
commit 228f3c65a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
158 changed files with 7235 additions and 916 deletions

View file

@ -4,6 +4,7 @@ import httpx
import pytest
import respx
from tests.conftest import build_test_vibe_config
from vibe.cli.plan_offer.adapters.http_whoami_gateway import HttpWhoAmIGateway
from vibe.cli.plan_offer.ports.whoami_gateway import (
WhoAmIGatewayError,
@ -11,6 +12,7 @@ from vibe.cli.plan_offer.ports.whoami_gateway import (
WhoAmIPlanType,
WhoAmIResponse,
)
from vibe.core.config import DEFAULT_CONSOLE_BASE_URL
@pytest.mark.asyncio
@ -170,3 +172,53 @@ async def test_return_unknown_plan_on_unsupported_plan_type(
plan_name="INDIVIDUAL",
prompt_switching_to_pro_plan=False,
)
@pytest.mark.asyncio
async def test_gateway_calls_custom_console_base_url_from_config(
respx_mock: respx.MockRouter,
) -> None:
custom_url = "https://custom-console.example.com"
config = build_test_vibe_config(console_base_url=custom_url)
route = respx_mock.get(f"{custom_url}/api/vibe/whoami").mock(
return_value=httpx.Response(
200,
json={
"plan_type": "CHAT",
"plan_name": "INDIVIDUAL",
"prompt_switching_to_pro_plan": False,
},
)
)
gateway = HttpWhoAmIGateway(base_url=config.console_base_url)
response = await gateway.whoami("api-key")
assert route.called
assert response.plan_type == "CHAT"
@pytest.mark.asyncio
async def test_gateway_uses_default_console_url_when_not_configured(
respx_mock: respx.MockRouter,
) -> None:
config = build_test_vibe_config()
assert config.console_base_url == DEFAULT_CONSOLE_BASE_URL
route = respx_mock.get(f"{DEFAULT_CONSOLE_BASE_URL}/api/vibe/whoami").mock(
return_value=httpx.Response(
200,
json={
"plan_type": "CHAT",
"plan_name": "INDIVIDUAL",
"prompt_switching_to_pro_plan": False,
},
)
)
gateway = HttpWhoAmIGateway(base_url=config.console_base_url)
await gateway.whoami("api-key")
assert route.called

View file

@ -0,0 +1,78 @@
from __future__ import annotations
from unittest.mock import patch
from pydantic import BaseModel
import pytest
from vibe.cli.textual_ui.widgets.approval_app import ApprovalApp
from vibe.core.config import VibeConfig
_TEST_GRACE_PERIOD_S = 0.5
class FakeArgs(BaseModel):
command: str = "echo hello"
@pytest.fixture
def approval_app(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"vibe.cli.textual_ui.widgets.approval_app._INPUT_GRACE_PERIOD_S",
_TEST_GRACE_PERIOD_S,
)
config = VibeConfig()
app = ApprovalApp(tool_name="bash", tool_args=FakeArgs(), config=config)
app._mount_time = 100.0
return app
class TestGracePeriod:
def test_actions_ignored_within_grace_period(self, approval_app: ApprovalApp):
with (
patch("vibe.cli.textual_ui.widgets.approval_app.time") as mock_time,
patch.object(approval_app, "post_message") as posted,
):
mock_time.monotonic.return_value = 100.0 + _TEST_GRACE_PERIOD_S - 0.01
assert approval_app.is_within_grace_period()
approval_app.action_select()
approval_app.action_select_1()
approval_app.action_select_2()
approval_app.action_select_3()
approval_app.action_reject()
posted.assert_not_called()
def test_actions_post_messages_after_grace_period(self, approval_app: ApprovalApp):
with (
patch("vibe.cli.textual_ui.widgets.approval_app.time") as mock_time,
patch.object(approval_app, "post_message") as posted,
):
mock_time.monotonic.return_value = 100.0 + _TEST_GRACE_PERIOD_S + 0.01
assert not approval_app.is_within_grace_period()
approval_app.action_select_1()
approval_app.action_reject()
assert posted.call_count == 2
assert isinstance(
posted.call_args_list[0].args[0], ApprovalApp.ApprovalGranted
)
assert isinstance(
posted.call_args_list[1].args[0], ApprovalApp.ApprovalRejected
)
def test_arrow_keys_work_during_grace_period(self, approval_app: ApprovalApp):
with (
patch("vibe.cli.textual_ui.widgets.approval_app.time") as mock_time,
patch.object(approval_app, "_update_options"),
):
mock_time.monotonic.return_value = 100.0 + 0.01
assert approval_app.is_within_grace_period()
assert approval_app.selected_option == 0
approval_app.action_move_down()
assert approval_app.selected_option == 1
approval_app.action_move_up()
assert approval_app.selected_option == 0

View file

@ -21,6 +21,7 @@ def _make_args(**overrides: object) -> argparse.Namespace:
"agent": "default",
"setup": False,
"workdir": None,
"add_dir": [],
"trust": False,
"teleport": False,
"continue_session": False,

View file

@ -0,0 +1,111 @@
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
from vibe.core.tools.builtins.ask_user_question import (
AskUserQuestionArgs,
Choice,
Question,
)
_TEST_GRACE_PERIOD_S = 0.5
@pytest.fixture
def question_app(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"vibe.cli.textual_ui.widgets.question_app._INPUT_GRACE_PERIOD_S",
_TEST_GRACE_PERIOD_S,
)
args = AskUserQuestionArgs(
questions=[
Question(
question="Pick one",
header="Pick",
options=[Choice(label="A"), Choice(label="B")],
)
]
)
app = QuestionApp(args)
app._mount_time = 100.0
return app
class TestQuestionAppGracePeriod:
def test_select_and_cancel_ignored_within_grace_period(
self, question_app: QuestionApp
):
with (
patch("vibe.cli.textual_ui.widgets.question_app.time") as mock_time,
patch.object(question_app, "post_message") as posted,
):
mock_time.monotonic.return_value = 100.0 + _TEST_GRACE_PERIOD_S - 0.01
assert question_app.is_within_grace_period()
question_app.action_select()
question_app.action_cancel()
posted.assert_not_called()
def test_cancel_posts_message_after_grace_period(self, question_app: QuestionApp):
with (
patch("vibe.cli.textual_ui.widgets.question_app.time") as mock_time,
patch.object(question_app, "post_message") as posted,
):
mock_time.monotonic.return_value = 100.0 + _TEST_GRACE_PERIOD_S + 0.01
question_app.action_cancel()
posted.assert_called_once()
assert isinstance(posted.call_args.args[0], QuestionApp.Cancelled)
def test_navigation_works_during_grace_period(self, question_app: QuestionApp):
with patch("vibe.cli.textual_ui.widgets.question_app.time") as mock_time:
mock_time.monotonic.return_value = 100.0 + 0.01
assert question_app.is_within_grace_period()
assert question_app.selected_option == 0
question_app.action_move_down()
assert question_app.selected_option == 1
question_app.action_move_up()
assert question_app.selected_option == 0
def test_number_key_consumed_but_not_acted_within_grace_period(
self, question_app: QuestionApp
):
with (
patch("vibe.cli.textual_ui.widgets.question_app.time") as mock_time,
patch.object(question_app, "post_message") as posted,
):
mock_time.monotonic.return_value = 100.0 + _TEST_GRACE_PERIOD_S - 0.01
event = MagicMock(spec=events.Key)
event.character = "1"
handled = question_app._handle_number_key(event)
assert handled is True
event.stop.assert_called_once()
event.prevent_default.assert_called_once()
posted.assert_not_called()
def test_number_key_selects_option_after_grace_period(
self, question_app: QuestionApp
):
with (
patch("vibe.cli.textual_ui.widgets.question_app.time") as mock_time,
patch.object(question_app, "post_message") as posted,
):
mock_time.monotonic.return_value = 100.0 + _TEST_GRACE_PERIOD_S + 0.01
event = MagicMock(spec=events.Key)
event.character = "1"
handled = question_app._handle_number_key(event)
assert handled is True
assert question_app.selected_option == 0
posted.assert_called_once()
assert isinstance(posted.call_args.args[0], QuestionApp.Answered)

View file

@ -0,0 +1,35 @@
from __future__ import annotations
import pytest
from vibe.cli.textual_ui.app import _TYPING_DEBOUNCE_ENV_VAR, _resolve_typing_debounce_s
_TEST_DEFAULT_DEBOUNCE_MS = 1000
@pytest.fixture(autouse=True)
def _restore_default_debounce(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"vibe.cli.textual_ui.app._DEFAULT_TYPING_DEBOUNCE_MS", _TEST_DEFAULT_DEBOUNCE_MS
)
class TestTypingDebounceEnvVar:
@pytest.mark.parametrize(
("env_value", "expected_s"), [("500", 0.5), ("2000", 2.0), ("0", 0.0)]
)
def test_env_var_override(
self, env_value: str, expected_s: float, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv(_TYPING_DEBOUNCE_ENV_VAR, env_value)
assert _resolve_typing_debounce_s() == expected_s
@pytest.mark.parametrize("env_value", [None, "not-a-number", "-100"])
def test_falls_back_to_default(
self, env_value: str | None, monkeypatch: pytest.MonkeyPatch
):
if env_value is None:
monkeypatch.delenv(_TYPING_DEBOUNCE_ENV_VAR, raising=False)
else:
monkeypatch.setenv(_TYPING_DEBOUNCE_ENV_VAR, env_value)
assert _resolve_typing_debounce_s() == _TEST_DEFAULT_DEBOUNCE_MS / 1000

View file

@ -3,13 +3,14 @@ from __future__ import annotations
import pytest
from vibe.cli.textual_ui.session_exit import print_session_resume_message
from vibe.core.config import SessionLoggingConfig
from vibe.core.types import AgentStats
def test_print_session_resume_message_skips_output_without_session_id(
capsys: pytest.CaptureFixture[str],
) -> None:
print_session_resume_message(None, AgentStats())
print_session_resume_message(None, AgentStats(), SessionLoggingConfig())
assert capsys.readouterr().out == ""
@ -20,6 +21,7 @@ def test_print_session_resume_message_prints_resume_commands_and_usage(
print_session_resume_message(
"12345678-1234-1234-1234-123456789abc",
AgentStats(session_prompt_tokens=14_867, session_completion_tokens=6),
SessionLoggingConfig(),
)
assert capsys.readouterr().out == (
@ -34,7 +36,7 @@ def test_print_session_resume_message_prints_resume_commands_and_usage(
def test_print_session_resume_message_prints_zero_usage_for_resumed_run_without_llm_activity(
capsys: pytest.CaptureFixture[str],
) -> None:
print_session_resume_message("12345678", AgentStats())
print_session_resume_message("12345678", AgentStats(), SessionLoggingConfig())
assert capsys.readouterr().out == (
"\n"