Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Mert Unsal <mertunsal1905@gmail.com>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Val <102326092+vdeva@users.noreply.github.com>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: maximevoisin-pm <maxime.voisin@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-05-27 17:10:40 +02:00 committed by GitHub
parent adb1ca74ce
commit cf3f4ca58f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
143 changed files with 3457 additions and 1351 deletions

View file

View file

@ -0,0 +1,17 @@
from __future__ import annotations
from vibe.cli.vscode_extension_promo import (
VscodeExtensionPromoRepository,
VscodeExtensionPromoState,
)
class FakeVscodeExtensionPromoRepository(VscodeExtensionPromoRepository):
def __init__(self, state: VscodeExtensionPromoState | None = None) -> None:
self.state: VscodeExtensionPromoState | None = state
async def get(self) -> VscodeExtensionPromoState | None:
return self.state
async def set(self, state: VscodeExtensionPromoState) -> None:
self.state = state

View file

@ -0,0 +1,156 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from pathlib import Path
import time
from unittest.mock import patch
import pytest
from tests.conftest import build_test_vibe_app, build_test_vibe_config
from tests.update_notifier.adapters.fake_update_cache_repository import (
FakeUpdateCacheRepository,
)
from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway
from tests.vscode_extension_promo.fake_repository import (
FakeVscodeExtensionPromoRepository,
)
from vibe.cli.textual_ui.app import VibeApp
from vibe.cli.textual_ui.widgets.messages import (
VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX,
VscodeExtensionPromoMessage,
WhatsNewMessage,
)
from vibe.cli.update_notifier import UpdateCache
from vibe.cli.vscode_extension_promo import (
VscodeExtensionPromo,
VscodeExtensionPromoState,
)
from vibe.core.config import VibeConfig
@pytest.fixture
def vibe_config() -> VibeConfig:
return build_test_vibe_config(enable_update_checks=True)
def _build_app(
*,
config: VibeConfig,
promo: VscodeExtensionPromo | None,
update_cache_repository: FakeUpdateCacheRepository,
current_version: str = "1.0.0",
) -> VibeApp:
return build_test_vibe_app(
config=config,
update_notifier=FakeUpdateGateway(update=None),
update_cache_repository=update_cache_repository,
current_version=current_version,
vscode_extension_promo=promo,
)
async def _wait_for(predicate: Callable[[], bool], pilot, timeout: float = 1.0) -> None:
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while loop.time() < deadline:
if predicate():
return
await pilot.pause(0.05)
pytest.fail("Condition not met within timeout")
@pytest.fixture(autouse=True)
def _bypass_promo_gates() -> object:
with (
patch("vibe.cli.textual_ui.app._is_vscode_family_terminal", return_value=True),
patch("vibe.cli.textual_ui.app.should_show_promo") as mocked_should_show,
):
mocked_should_show.side_effect = lambda state: (
state is None or state.shown_count < 10
)
yield mocked_should_show
@pytest.mark.asyncio
async def test_promo_appears_as_standalone_chat_message_when_no_whats_new(
vibe_config: VibeConfig, tmp_path: Path
) -> None:
repository = FakeVscodeExtensionPromoRepository()
promo = VscodeExtensionPromo(repository=repository, initial_state=None)
cache = UpdateCache(
latest_version="1.0.0",
stored_at_timestamp=int(time.time()),
seen_whats_new_version="1.0.0",
)
app = _build_app(
config=vibe_config,
promo=promo,
update_cache_repository=FakeUpdateCacheRepository(update_cache=cache),
)
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
async with app.run_test() as pilot:
await pilot.pause(0.3)
await _wait_for(lambda: bool(app.query(VscodeExtensionPromoMessage)), pilot)
assert not app.query(WhatsNewMessage)
assert repository.state == VscodeExtensionPromoState(shown_count=1)
@pytest.mark.asyncio
async def test_promo_is_merged_into_whats_new_message_when_both_shown(
vibe_config: VibeConfig, tmp_path: Path
) -> None:
repository = FakeVscodeExtensionPromoRepository()
promo = VscodeExtensionPromo(repository=repository, initial_state=None)
cache = UpdateCache(
latest_version="1.0.0",
stored_at_timestamp=int(time.time()),
seen_whats_new_version=None,
)
app = _build_app(
config=vibe_config,
promo=promo,
update_cache_repository=FakeUpdateCacheRepository(update_cache=cache),
)
whats_new_content = "# What's New\n\n- Feature 1"
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
(tmp_path / "whats_new.md").write_text(whats_new_content)
async with app.run_test() as pilot:
await pilot.pause(0.3)
await _wait_for(lambda: bool(app.query(WhatsNewMessage)), pilot)
message = app.query_one(WhatsNewMessage)
await _wait_for(
lambda: repository.state == VscodeExtensionPromoState(shown_count=1),
pilot,
)
assert not app.query(VscodeExtensionPromoMessage)
assert message._content.startswith(whats_new_content)
assert message._content.endswith(VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX)
@pytest.mark.asyncio
async def test_promo_does_not_appear_when_no_promo_dependency_provided(
vibe_config: VibeConfig, tmp_path: Path
) -> None:
cache = UpdateCache(
latest_version="1.0.0",
stored_at_timestamp=int(time.time()),
seen_whats_new_version="1.0.0",
)
app = _build_app(
config=vibe_config,
promo=None,
update_cache_repository=FakeUpdateCacheRepository(update_cache=cache),
)
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
async with app.run_test() as pilot:
await pilot.pause(0.4)
assert not app.query(VscodeExtensionPromoMessage)
assert not app.query(WhatsNewMessage)

View file

@ -0,0 +1,74 @@
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from pathlib import Path
import tomllib
import pytest
import tomli_w
from vibe.cli.vscode_extension_promo import (
MAX_SHOWN_COUNT,
PROMO_START,
FileSystemVscodeExtensionPromoRepository,
VscodeExtensionPromoState,
should_show_promo,
)
@pytest.mark.asyncio
async def test_repository_roundtrip_persists_shown_count(tmp_path: Path) -> None:
repository = FileSystemVscodeExtensionPromoRepository(base_path=tmp_path)
await repository.set(VscodeExtensionPromoState(shown_count=4))
assert await repository.get() == VscodeExtensionPromoState(shown_count=4)
@pytest.mark.asyncio
async def test_repository_set_preserves_unrelated_cache_sections(
tmp_path: Path,
) -> None:
with (tmp_path / "cache.toml").open("wb") as f:
tomli_w.dump({"update_cache": {"latest_version": "2.0.0"}}, f)
repository = FileSystemVscodeExtensionPromoRepository(base_path=tmp_path)
await repository.set(VscodeExtensionPromoState(shown_count=1))
with (tmp_path / "cache.toml").open("rb") as f:
data = tomllib.load(f)
assert data["update_cache"]["latest_version"] == "2.0.0"
assert data["vscode_extension_promo"]["shown_count"] == 1
@pytest.mark.parametrize(
"state,expected",
[
(None, True),
(VscodeExtensionPromoState(shown_count=0), True),
(VscodeExtensionPromoState(shown_count=MAX_SHOWN_COUNT - 1), True),
(VscodeExtensionPromoState(shown_count=MAX_SHOWN_COUNT), False),
(VscodeExtensionPromoState(shown_count=MAX_SHOWN_COUNT + 5), False),
],
)
def test_should_show_promo_respects_threshold(
state: VscodeExtensionPromoState | None, expected: bool
) -> None:
assert should_show_promo(state, now=PROMO_START) is expected
@pytest.mark.parametrize(
"now,expected",
[
(PROMO_START - timedelta(seconds=1), False),
(PROMO_START - timedelta(days=365), False),
(PROMO_START, True),
(PROMO_START + timedelta(seconds=1), True),
],
)
def test_should_show_promo_gated_by_start_date(now: datetime, expected: bool) -> None:
assert should_show_promo(None, now=now) is expected
def test_promo_start_is_2026_05_28_16_utc() -> None:
assert PROMO_START == datetime(2026, 5, 28, 16, 0, tzinfo=UTC)