Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Gauthier Guinet <43207538+Gguinet@users.noreply.github.com>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Quentin <torroba.q@gmail.com>
Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com>
Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-03-23 18:45:21 +01:00 committed by GitHub
parent 5103019b01
commit eb580209d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
180 changed files with 11136 additions and 1030 deletions

View file

@ -13,7 +13,8 @@ from vibe.cli.plan_offer.decide_plan_offer import (
resolve_api_key_for_plan,
)
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
from vibe.core.config import Backend, ProviderConfig
from vibe.core.config import ProviderConfig
from vibe.core.types import Backend
@pytest.fixture

View file

@ -8,7 +8,7 @@ class TestCommandRegistry:
registry = CommandRegistry()
assert registry.get_command_name("/help") == "help"
assert registry.get_command_name("/config") == "config"
assert registry.get_command_name("/model") == "config"
assert registry.get_command_name("/model") == "model"
assert registry.get_command_name("/clear") == "clear"
assert registry.get_command_name("/exit") == "exit"

View file

@ -0,0 +1,77 @@
from __future__ import annotations
from unittest.mock import MagicMock, patch
from vibe.cli.textual_ui.widgets.feedback_bar import FeedbackBar
class TestFeedbackBarState:
def test_maybe_show_shows_when_random_below_threshold(self):
bar = FeedbackBar()
bar.display = False
bar._set_active = MagicMock()
with patch(
"vibe.cli.textual_ui.widgets.feedback_bar.random.random", return_value=0
):
bar.maybe_show()
bar._set_active.assert_called_once_with(True)
def test_maybe_show_does_not_show_when_random_above_threshold(self):
bar = FeedbackBar()
bar.display = False
bar._set_active = MagicMock()
with patch(
"vibe.cli.textual_ui.widgets.feedback_bar.random.random", return_value=1.0
):
bar.maybe_show()
bar._set_active.assert_not_called()
def test_hide_calls_set_active_false_when_displayed(self):
bar = FeedbackBar()
bar.display = True
bar._set_active = MagicMock()
bar.hide()
bar._set_active.assert_called_once_with(False)
def test_hide_does_nothing_when_already_hidden(self):
bar = FeedbackBar()
bar.display = False
bar._set_active = MagicMock()
bar.hide()
bar._set_active.assert_not_called()
def test_handle_feedback_key_posts_message_and_deactivates(self):
bar = FeedbackBar()
bar.set_timer = MagicMock()
bar.post_message = MagicMock()
bar.query_one = MagicMock()
mock_text_area = MagicMock()
mock_text_area.feedback_active = True
mock_app = MagicMock()
mock_app.query_one.return_value = mock_text_area
with patch.object(
type(bar), "app", new_callable=lambda: property(lambda self: mock_app)
):
bar.handle_feedback_key(3)
assert mock_text_area.feedback_active is False
bar.post_message.assert_called_once()
msg = bar.post_message.call_args[0][0]
assert isinstance(msg, FeedbackBar.FeedbackGiven)
assert msg.rating == 3
bar.set_timer.assert_called_once()
class TestFeedbackGivenMessage:
def test_message_stores_rating(self):
msg = FeedbackBar.FeedbackGiven(rating=2)
assert msg.rating == 2

View file

@ -0,0 +1,284 @@
from __future__ import annotations
from unittest.mock import patch
import pytest
from tests.conftest import build_test_vibe_app, build_test_vibe_config
from vibe.cli.textual_ui.app import BottomApp
from vibe.cli.textual_ui.widgets.config_app import ConfigApp
from vibe.cli.textual_ui.widgets.model_picker import ModelPickerApp
from vibe.core.config._settings import ModelConfig
def _make_config_with_models():
models = [
ModelConfig(name="model-a", provider="mistral", alias="alpha"),
ModelConfig(name="model-b", provider="mistral", alias="beta"),
ModelConfig(name="model-c", provider="mistral", alias="gamma"),
]
return build_test_vibe_config(models=models, active_model="alpha")
# --- /config command ---
@pytest.mark.asyncio
async def test_config_opens_config_app() -> None:
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_config()
await pilot.pause(0.2)
assert app._current_bottom_app == BottomApp.Config
assert len(app.query(ConfigApp)) == 1
@pytest.mark.asyncio
async def test_config_escape_returns_to_input() -> None:
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_config()
await pilot.pause(0.2)
await pilot.press("escape")
await pilot.pause(0.2)
assert app._current_bottom_app == BottomApp.Input
assert len(app.query(ConfigApp)) == 0
@pytest.mark.asyncio
async def test_config_toggle_autocopy() -> None:
config = _make_config_with_models()
config.autocopy_to_clipboard = False
app = build_test_vibe_app(config=config)
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_config()
await pilot.pause(0.2)
# Navigate down to Auto-copy (second item) and toggle
await pilot.press("down")
await pilot.press("enter")
await pilot.pause(0.1)
# Verify the toggle happened in the widget
config_app = app.query_one(ConfigApp)
assert config_app.changes.get("autocopy_to_clipboard") == "On"
@pytest.mark.asyncio
async def test_config_escape_saves_changes() -> None:
config = _make_config_with_models()
config.autocopy_to_clipboard = False
app = build_test_vibe_app(config=config)
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_config()
await pilot.pause(0.2)
# Toggle auto-copy
await pilot.press("down")
await pilot.press("enter")
await pilot.pause(0.1)
with patch("vibe.cli.textual_ui.app.VibeConfig.save_updates") as mock_save:
await pilot.press("escape")
await pilot.pause(0.2)
mock_save.assert_called_once()
changes = mock_save.call_args[0][0]
assert changes["autocopy_to_clipboard"] is True
# --- /model command ---
@pytest.mark.asyncio
async def test_model_opens_model_picker() -> None:
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_model()
await pilot.pause(0.2)
assert app._current_bottom_app == BottomApp.ModelPicker
assert len(app.query(ModelPickerApp)) == 1
@pytest.mark.asyncio
async def test_model_picker_shows_all_models() -> None:
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_model()
await pilot.pause(0.2)
picker = app.query_one(ModelPickerApp)
assert picker._model_aliases == ["alpha", "beta", "gamma"]
assert picker._current_model == "alpha"
@pytest.mark.asyncio
async def test_model_picker_escape_returns_to_input() -> None:
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_model()
await pilot.pause(0.2)
await pilot.press("escape")
await pilot.pause(0.2)
assert app._current_bottom_app == BottomApp.Input
assert len(app.query(ModelPickerApp)) == 0
@pytest.mark.asyncio
async def test_model_picker_escape_does_not_save() -> None:
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_model()
await pilot.pause(0.2)
with patch("vibe.cli.textual_ui.app.VibeConfig.save_updates") as mock_save:
await pilot.press("escape")
await pilot.pause(0.2)
mock_save.assert_not_called()
@pytest.mark.asyncio
async def test_model_picker_select_model() -> None:
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_model()
await pilot.pause(0.2)
# Navigate down to "beta" and select
await pilot.press("down")
with patch("vibe.cli.textual_ui.app.VibeConfig.save_updates") as mock_save:
await pilot.press("enter")
await pilot.pause(0.2)
mock_save.assert_called_once_with({"active_model": "beta"})
assert app._current_bottom_app == BottomApp.Input
assert len(app.query(ModelPickerApp)) == 0
@pytest.mark.asyncio
async def test_model_picker_select_current_model() -> None:
"""Selecting the already-active model still saves (idempotent)."""
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_model()
await pilot.pause(0.2)
with patch("vibe.cli.textual_ui.app.VibeConfig.save_updates") as mock_save:
await pilot.press("enter")
await pilot.pause(0.2)
mock_save.assert_called_once_with({"active_model": "alpha"})
assert app._current_bottom_app == BottomApp.Input
# --- config -> model picker flow ---
@pytest.mark.asyncio
async def test_config_model_entry_opens_model_picker() -> None:
"""Pressing Enter on the Model row in /config opens the model picker."""
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_config()
await pilot.pause(0.2)
# Model row is the first item, already highlighted. Press enter.
await pilot.press("enter")
await pilot.pause(0.3)
assert app._current_bottom_app == BottomApp.ModelPicker
assert len(app.query(ModelPickerApp)) == 1
assert len(app.query(ConfigApp)) == 0
@pytest.mark.asyncio
async def test_config_to_model_picker_escape_returns_to_input() -> None:
"""Opening model picker from config, then ESC, returns to input (not config)."""
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_config()
await pilot.pause(0.2)
# Open model picker from config
await pilot.press("enter")
await pilot.pause(0.3)
# Escape model picker
await pilot.press("escape")
await pilot.pause(0.2)
assert app._current_bottom_app == BottomApp.Input
assert len(app.query(ModelPickerApp)) == 0
assert len(app.query(ConfigApp)) == 0
@pytest.mark.asyncio
async def test_config_to_model_picker_select_returns_to_input() -> None:
"""Opening model picker from config, selecting a model, returns to input."""
app = build_test_vibe_app(config=_make_config_with_models())
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_config()
await pilot.pause(0.2)
# Open model picker from config
await pilot.press("enter")
await pilot.pause(0.3)
# Select second model
await pilot.press("down")
with patch("vibe.cli.textual_ui.app.VibeConfig.save_updates") as mock_save:
await pilot.press("enter")
await pilot.pause(0.2)
mock_save.assert_called_once_with({"active_model": "beta"})
assert app._current_bottom_app == BottomApp.Input
@pytest.mark.asyncio
async def test_config_pending_changes_saved_before_model_picker() -> None:
"""Toggle changes in config are saved before switching to model picker."""
config = _make_config_with_models()
config.autocopy_to_clipboard = False
app = build_test_vibe_app(config=config)
async with app.run_test() as pilot:
await pilot.pause(0.1)
await app._show_config()
await pilot.pause(0.2)
# Toggle auto-copy (second row)
await pilot.press("down")
await pilot.press("enter")
await pilot.pause(0.1)
# Go back up to model row and open model picker
await pilot.press("up")
with patch("vibe.cli.textual_ui.app.VibeConfig.save_updates") as mock_save:
await pilot.press("enter")
await pilot.pause(0.3)
mock_save.assert_called_once()
changes = mock_save.call_args[0][0]
assert changes["autocopy_to_clipboard"] is True

View file

@ -0,0 +1,45 @@
from __future__ import annotations
import pytest
from vibe.cli.textual_ui.session_exit import print_session_resume_message
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())
assert capsys.readouterr().out == ""
def test_print_session_resume_message_prints_resume_commands_and_usage(
capsys: pytest.CaptureFixture[str],
) -> None:
print_session_resume_message(
"12345678-1234-1234-1234-123456789abc",
AgentStats(session_prompt_tokens=14_867, session_completion_tokens=6),
)
assert capsys.readouterr().out == (
"\n"
"Total tokens used this session: input=14,867 output=6 (total=14,873)\n"
"\n"
"To continue this session, run: vibe --continue\n"
"Or: vibe --resume 12345678-1234-1234-1234-123456789abc\n"
)
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())
assert capsys.readouterr().out == (
"\n"
"Total tokens used this session: input=0 output=0 (total=0)\n"
"\n"
"To continue this session, run: vibe --continue\n"
"Or: vibe --resume 12345678\n"
)