Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai>
Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai>
Co-Authored-By: Clément Drouin <clement.drouin@mistral.ai>
Co-Authored-By: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-Authored-By: Clément Siriex <clement.sirieix@mistral.ai>
Co-Authored-By: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-Authored-By: Thaddee Tyl <thaddee.tyl@gmail.com>
Co-Authored-By: David Brochart <david.brochart@gmail.com>
Co-Authored-By: Joseph Guhlin <joseph.guhlin@gmail.com>
Co-Authored-By: Thomas Kenbeek <thomaskenbeek@gmail.com>
Co-Authored-By: Remenby31 <baptiste.cruvellier31@gmail.com>
This commit is contained in:
Mathias Gesbert 2026-01-27 16:39:30 +01:00 committed by Mathias Gesbert
parent 79f215d91c
commit d33db9fff8
217 changed files with 16911 additions and 4305 deletions

View file

@ -0,0 +1,406 @@
from __future__ import annotations
import asyncio
from pathlib import Path
import time
from typing import Protocol
from unittest.mock import patch
import pytest
from textual.app import Notification
from tests.update_notifier.adapters.fake_update_cache_repository import (
FakeUpdateCacheRepository,
)
from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway
from vibe.cli.textual_ui.app import VibeApp
from vibe.cli.textual_ui.widgets.messages import WhatsNewMessage
from vibe.cli.update_notifier import (
Update,
UpdateCache,
UpdateGatewayCause,
UpdateGatewayError,
)
from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.config import SessionLoggingConfig, VibeConfig
async def _wait_for_notification(
app: VibeApp, pilot, *, timeout: float = 1.0, interval: float = 0.05
) -> Notification:
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while loop.time() < deadline:
notifications = list(app._notifications)
if notifications:
return notifications[-1]
await pilot.pause(interval)
pytest.fail("Notification not displayed")
async def _assert_no_notifications(
app: VibeApp, pilot, *, timeout: float = 1.0, interval: float = 0.05
) -> None:
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while loop.time() < deadline:
if app._notifications:
pytest.fail("Notification unexpectedly displayed")
await pilot.pause(interval)
assert not app._notifications
@pytest.fixture
def vibe_config_with_update_checks_enabled() -> VibeConfig:
return VibeConfig(
session_logging=SessionLoggingConfig(enabled=False), enable_update_checks=True
)
class VibeAppFactory(Protocol):
def __call__(
self,
*,
notifier: FakeUpdateGateway,
update_cache_repository: FakeUpdateCacheRepository | None = None,
config: VibeConfig | None = None,
initial_agent_name: str = BuiltinAgentName.DEFAULT,
current_version: str = "0.1.0",
) -> VibeApp: ...
@pytest.fixture
def make_vibe_app(vibe_config_with_update_checks_enabled: VibeConfig) -> VibeAppFactory:
update_cache_repository = FakeUpdateCacheRepository()
def _make_app(
*,
notifier: FakeUpdateGateway,
update_cache_repository: FakeUpdateCacheRepository
| None = update_cache_repository,
config: VibeConfig | None = None,
initial_agent_name: str = BuiltinAgentName.DEFAULT,
current_version: str = "0.1.0",
) -> VibeApp:
app_config = config or vibe_config_with_update_checks_enabled
agent_loop = AgentLoop(
app_config, agent_name=initial_agent_name, enable_streaming=False
)
return VibeApp(
agent_loop=agent_loop,
update_notifier=notifier,
update_cache_repository=update_cache_repository,
current_version=current_version,
)
return _make_app
@pytest.mark.asyncio
async def test_ui_displays_update_notification(make_vibe_app: VibeAppFactory) -> None:
notifier = FakeUpdateGateway(update=Update(latest_version="0.2.0"))
app = make_vibe_app(notifier=notifier)
async with app.run_test() as pilot:
notification = await _wait_for_notification(app, pilot, timeout=0.3)
assert notification.severity == "information"
assert notification.title == "Update available"
assert (
notification.message
== "0.1.0 => 0.2.0\nPlease update mistral-vibe with your package manager"
)
@pytest.mark.asyncio
async def test_ui_does_not_display_update_notification_when_not_available(
make_vibe_app: VibeAppFactory,
) -> None:
notifier = FakeUpdateGateway(update=None)
app = make_vibe_app(notifier=notifier)
async with app.run_test() as pilot:
await _assert_no_notifications(app, pilot, timeout=0.3)
assert notifier.fetch_update_calls == 1
@pytest.mark.asyncio
async def test_ui_displays_warning_toast_when_check_fails(
make_vibe_app: VibeAppFactory,
) -> None:
notifier = FakeUpdateGateway(
error=UpdateGatewayError(cause=UpdateGatewayCause.FORBIDDEN)
)
app = make_vibe_app(notifier=notifier)
async with app.run_test() as pilot:
await pilot.pause(0.3)
notifications = list(app._notifications)
assert notifications
warning = notifications[-1]
assert warning.severity == "warning"
assert "forbidden" in warning.message.lower()
@pytest.mark.asyncio
async def test_ui_does_not_invoke_gateway_nor_show_error_notification_when_update_checks_are_disabled(
vibe_config_with_update_checks_enabled: VibeConfig, make_vibe_app: VibeAppFactory
) -> None:
config = vibe_config_with_update_checks_enabled
config.enable_update_checks = False
notifier = FakeUpdateGateway(update=Update(latest_version="0.2.0"))
app = make_vibe_app(notifier=notifier, config=config)
async with app.run_test() as pilot:
await _assert_no_notifications(app, pilot, timeout=0.3)
assert notifier.fetch_update_calls == 0
@pytest.mark.asyncio
async def test_ui_does_not_show_toast_when_update_is_known_in_recent_cache_already(
make_vibe_app: VibeAppFactory,
) -> None:
timestamp_two_hours_ago = int(time.time()) - 2 * 60 * 60
notifier = FakeUpdateGateway(update=Update(latest_version="0.2.0"))
update_cache = UpdateCache(
latest_version="0.2.0", stored_at_timestamp=timestamp_two_hours_ago
)
update_cache_repository = FakeUpdateCacheRepository(update_cache=update_cache)
app = make_vibe_app(
notifier=notifier, update_cache_repository=update_cache_repository
)
async with app.run_test() as pilot:
await _assert_no_notifications(app, pilot, timeout=0.3)
assert notifier.fetch_update_calls == 0
@pytest.mark.asyncio
async def test_ui_does_show_toast_when_cache_entry_is_too_old(
make_vibe_app: VibeAppFactory,
) -> None:
timestamp_two_days_ago = int(time.time()) - 2 * 24 * 60 * 60
notifier = FakeUpdateGateway(update=Update(latest_version="0.2.0"))
update_cache = UpdateCache(
latest_version="0.2.0", stored_at_timestamp=timestamp_two_days_ago
)
update_cache_repository = FakeUpdateCacheRepository(update_cache=update_cache)
app = make_vibe_app(
notifier=notifier, update_cache_repository=update_cache_repository
)
async with app.run_test() as pilot:
await pilot.pause(0.3)
notifications = list(app._notifications)
assert notifications
notification = notifications[-1]
assert notification.severity == "information"
assert notification.title == "Update available"
assert (
notification.message
== "0.1.0 => 0.2.0\nPlease update mistral-vibe with your package manager"
)
assert notifier.fetch_update_calls == 1
async def _wait_for_whats_new_message(
app: VibeApp, pilot, *, timeout: float = 1.0, interval: float = 0.05
) -> WhatsNewMessage:
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while loop.time() < deadline:
try:
message = app.query_one(WhatsNewMessage)
if message:
return message
except Exception:
pass
await pilot.pause(interval)
pytest.fail("WhatsNewMessage not displayed")
@pytest.mark.asyncio
async def test_ui_displays_whats_new_message_when_content_exists(
make_vibe_app: VibeAppFactory, tmp_path: Path
) -> None:
notifier = FakeUpdateGateway(update=None)
cache = UpdateCache(
latest_version="1.0.0",
stored_at_timestamp=int(time.time()),
seen_whats_new_version=None,
)
repository = FakeUpdateCacheRepository(update_cache=cache)
app = make_vibe_app(
notifier=notifier, update_cache_repository=repository, current_version="1.0.0"
)
whats_new_content = "# What's New\n\n- Feature 1\n- Feature 2"
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
whats_new_file = tmp_path / "whats_new.md"
whats_new_file.write_text(whats_new_content)
async with app.run_test() as pilot:
await pilot.pause(0.5)
message = await _wait_for_whats_new_message(app, pilot, timeout=0.5)
assert message is not None
assert message._content == whats_new_content
assert repository.update_cache is not None
assert repository.update_cache.seen_whats_new_version == "1.0.0"
@pytest.mark.asyncio
async def test_ui_does_not_display_whats_new_when_seen_whats_new_version_matches(
make_vibe_app: VibeAppFactory, tmp_path: Path
) -> None:
notifier = FakeUpdateGateway(update=None)
cache = UpdateCache(
latest_version="1.0.0",
stored_at_timestamp=int(time.time()),
seen_whats_new_version="1.0.0",
)
repository = FakeUpdateCacheRepository(update_cache=cache)
app = make_vibe_app(
notifier=notifier, update_cache_repository=repository, current_version="1.0.0"
)
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
whats_new_file = tmp_path / "whats_new.md"
whats_new_file.write_text("# What's New\n\n- Feature 1")
async with app.run_test() as pilot:
await pilot.pause(0.5)
try:
app.query_one(WhatsNewMessage)
pytest.fail("WhatsNewMessage should not be displayed")
except Exception:
pass
@pytest.mark.asyncio
async def test_ui_does_not_display_whats_new_when_file_is_empty(
make_vibe_app: VibeAppFactory, tmp_path: Path
) -> None:
notifier = FakeUpdateGateway(update=None)
cache = UpdateCache(
latest_version="1.0.0",
stored_at_timestamp=int(time.time()),
seen_whats_new_version=None,
)
repository = FakeUpdateCacheRepository(update_cache=cache)
app = make_vibe_app(
notifier=notifier, update_cache_repository=repository, current_version="1.0.0"
)
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
whats_new_file = tmp_path / "whats_new.md"
whats_new_file.write_text("")
async with app.run_test() as pilot:
await pilot.pause(0.5)
try:
app.query_one(WhatsNewMessage)
pytest.fail("WhatsNewMessage should not be displayed")
except Exception:
pass # Expected: message should not exist
assert repository.update_cache is not None
assert repository.update_cache.seen_whats_new_version == "1.0.0"
@pytest.mark.asyncio
async def test_ui_does_not_display_whats_new_when_file_does_not_exist(
make_vibe_app: VibeAppFactory, tmp_path: Path
) -> None:
notifier = FakeUpdateGateway(update=None)
cache = UpdateCache(
latest_version="1.0.0",
stored_at_timestamp=int(time.time()),
seen_whats_new_version=None,
)
repository = FakeUpdateCacheRepository(update_cache=cache)
app = make_vibe_app(
notifier=notifier, update_cache_repository=repository, current_version="1.0.0"
)
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
async with app.run_test() as pilot:
await pilot.pause(0.5)
try:
app.query_one(WhatsNewMessage)
pytest.fail("WhatsNewMessage should not be displayed")
except Exception:
pass # Expected: message should not exist
assert repository.update_cache is not None
assert repository.update_cache.seen_whats_new_version == "1.0.0"
@pytest.mark.asyncio
async def test_ui_displays_success_notification_when_auto_update_succeeds(
make_vibe_app: VibeAppFactory,
) -> None:
config = VibeConfig(
session_logging=SessionLoggingConfig(enabled=False),
enable_update_checks=True,
enable_auto_update=True,
)
notifier = FakeUpdateGateway(update=Update(latest_version="0.2.0"))
with patch("vibe.cli.update_notifier.update.UPDATE_COMMANDS", ["true"]):
app = make_vibe_app(notifier=notifier, config=config)
async with app.run_test() as pilot:
await pilot.pause(0.3)
notifications = list(app._notifications)
assert notifications, "No notifications displayed"
notification = notifications[-1]
assert notification.severity == "information"
assert notification.title == "Update successful"
assert (
notification.message
== "0.1.0 => 0.2.0\nVibe was updated successfully. Please restart to use the new version."
)
@pytest.mark.asyncio
async def test_ui_displays_update_notification_when_auto_update_fails(
make_vibe_app: VibeAppFactory,
) -> None:
config = VibeConfig(
session_logging=SessionLoggingConfig(enabled=False),
enable_update_checks=True,
enable_auto_update=True,
)
notifier = FakeUpdateGateway(update=Update(latest_version="0.2.0"))
with patch("vibe.cli.update_notifier.update.UPDATE_COMMANDS", ["false"]):
app = make_vibe_app(notifier=notifier, config=config)
async with app.run_test() as pilot:
await pilot.pause(0.3)
notifications = list(app._notifications)
assert notifications
notification = notifications[-1]
assert notification.severity == "information"
assert notification.title == "Update available"
assert (
notification.message
== "0.1.0 => 0.2.0\nPlease update mistral-vibe with your package manager"
)