v2.6.0 (#524)
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:
parent
5103019b01
commit
eb580209d4
180 changed files with 11136 additions and 1030 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
77
tests/cli/test_feedback_bar.py
Normal file
77
tests/cli/test_feedback_bar.py
Normal 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
|
||||
284
tests/cli/test_ui_config_and_model_picker.py
Normal file
284
tests/cli/test_ui_config_and_model_picker.py
Normal 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
|
||||
45
tests/cli/test_ui_session_exit.py
Normal file
45
tests/cli/test_ui_session_exit.py
Normal 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"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue