2.0.0
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:
parent
79f215d91c
commit
d33db9fff8
217 changed files with 16911 additions and 4305 deletions
22
tests/update_notifier/adapters/fake_update_gateway.py
Normal file
22
tests/update_notifier/adapters/fake_update_gateway.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.update_notifier.ports.update_gateway import (
|
||||
Update,
|
||||
UpdateGateway,
|
||||
UpdateGatewayError,
|
||||
)
|
||||
|
||||
|
||||
class FakeUpdateGateway(UpdateGateway):
|
||||
def __init__(
|
||||
self, update: Update | None = None, error: UpdateGatewayError | None = None
|
||||
) -> None:
|
||||
self._update: Update | None = update
|
||||
self._error = error
|
||||
self.fetch_update_calls = 0
|
||||
|
||||
async def fetch_update(self) -> Update | None:
|
||||
self.fetch_update_calls += 1
|
||||
if self._error is not None:
|
||||
raise self._error
|
||||
return self._update
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.update_notifier.ports.version_update_gateway import (
|
||||
VersionUpdate,
|
||||
VersionUpdateGateway,
|
||||
VersionUpdateGatewayError,
|
||||
)
|
||||
|
||||
|
||||
class FakeVersionUpdateGateway(VersionUpdateGateway):
|
||||
def __init__(
|
||||
self,
|
||||
update: VersionUpdate | None = None,
|
||||
error: VersionUpdateGatewayError | None = None,
|
||||
) -> None:
|
||||
self._update: VersionUpdate | None = update
|
||||
self._error = error
|
||||
self.fetch_update_calls = 0
|
||||
|
||||
async def fetch_update(self) -> VersionUpdate | None:
|
||||
self.fetch_update_calls += 1
|
||||
if self._error is not None:
|
||||
raise self._error
|
||||
return self._update
|
||||
74
tests/update_notifier/test_do_update.py
Normal file
74
tests/update_notifier/test_do_update.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.cli.update_notifier.update import do_update
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_do_update_returns_true_when_first_command_succeeds() -> None:
|
||||
mock_process = MagicMock()
|
||||
mock_process.wait = AsyncMock(return_value=None)
|
||||
mock_process.returncode = 0
|
||||
|
||||
with patch(
|
||||
"vibe.cli.update_notifier.update.UPDATE_COMMANDS", ["command_1", "command_2"]
|
||||
):
|
||||
with patch(
|
||||
"vibe.cli.update_notifier.update.asyncio.create_subprocess_shell"
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_process
|
||||
|
||||
result = await do_update()
|
||||
|
||||
assert result is True
|
||||
mock_create.assert_called_once()
|
||||
assert "command_1" in mock_create.call_args[0][0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_do_update_returns_true_when_second_command_succeeds() -> None:
|
||||
mock_process_fail = MagicMock()
|
||||
mock_process_fail.wait = AsyncMock(return_value=None)
|
||||
mock_process_fail.returncode = 1
|
||||
|
||||
mock_process_success = MagicMock()
|
||||
mock_process_success.wait = AsyncMock(return_value=None)
|
||||
mock_process_success.returncode = 0
|
||||
|
||||
with patch(
|
||||
"vibe.cli.update_notifier.update.UPDATE_COMMANDS", ["command_1", "command_2"]
|
||||
):
|
||||
with patch(
|
||||
"vibe.cli.update_notifier.update.asyncio.create_subprocess_shell"
|
||||
) as mock_create:
|
||||
mock_create.side_effect = [mock_process_fail, mock_process_success]
|
||||
|
||||
result = await do_update()
|
||||
|
||||
assert result is True
|
||||
assert mock_create.call_count == 2
|
||||
assert "command_1" in mock_create.call_args_list[0][0][0]
|
||||
assert "command_2" in mock_create.call_args_list[1][0][0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_do_update_returns_false_when_all_commands_fail() -> None:
|
||||
mock_process = MagicMock()
|
||||
mock_process.wait = AsyncMock(return_value=None)
|
||||
mock_process.returncode = 1
|
||||
|
||||
with patch(
|
||||
"vibe.cli.update_notifier.update.UPDATE_COMMANDS", ["command_1", "command_2"]
|
||||
):
|
||||
with patch(
|
||||
"vibe.cli.update_notifier.update.asyncio.create_subprocess_shell"
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_process
|
||||
|
||||
result = await do_update()
|
||||
|
||||
assert result is False
|
||||
assert mock_create.call_count == 2
|
||||
|
|
@ -24,6 +24,7 @@ async def test_reads_cache_from_file_when_present(tmp_path: Path) -> None:
|
|||
assert cache is not None
|
||||
assert cache.latest_version == "1.2.3"
|
||||
assert cache.stored_at_timestamp == 1_700_000_000
|
||||
assert cache.seen_whats_new_version is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -62,6 +63,46 @@ async def test_overwrites_existing_cache(tmp_path: Path) -> None:
|
|||
content = json.loads(cache_file.read_text())
|
||||
assert content["latest_version"] == "1.1.0"
|
||||
assert content["stored_at_timestamp"] == 1_700_200_000
|
||||
assert content.get("seen_whats_new_version") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reads_cache_with_seen_whats_new_version(tmp_path: Path) -> None:
|
||||
cache_file = tmp_path / "update_cache.json"
|
||||
cache_file.write_text(
|
||||
json.dumps({
|
||||
"latest_version": "1.2.3",
|
||||
"stored_at_timestamp": 1_700_000_000,
|
||||
"seen_whats_new_version": "1.2.0",
|
||||
})
|
||||
)
|
||||
repository = FileSystemUpdateCacheRepository(base_path=tmp_path)
|
||||
|
||||
cache = await repository.get()
|
||||
|
||||
assert cache is not None
|
||||
assert cache.latest_version == "1.2.3"
|
||||
assert cache.stored_at_timestamp == 1_700_000_000
|
||||
assert cache.seen_whats_new_version == "1.2.0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_writes_cache_with_seen_whats_new_version(tmp_path: Path) -> None:
|
||||
repository = FileSystemUpdateCacheRepository(base_path=tmp_path)
|
||||
|
||||
await repository.set(
|
||||
UpdateCache(
|
||||
latest_version="1.1.0",
|
||||
stored_at_timestamp=1_700_200_000,
|
||||
seen_whats_new_version="1.1.0",
|
||||
)
|
||||
)
|
||||
|
||||
cache_file = tmp_path / "update_cache.json"
|
||||
content = json.loads(cache_file.read_text())
|
||||
assert content["latest_version"] == "1.1.0"
|
||||
assert content["stored_at_timestamp"] == 1_700_200_000
|
||||
assert content["seen_whats_new_version"] == "1.1.0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import httpx
|
|||
import pytest
|
||||
|
||||
from vibe.cli.update_notifier import (
|
||||
GitHubVersionUpdateGateway,
|
||||
VersionUpdateGatewayCause,
|
||||
VersionUpdateGatewayError,
|
||||
GitHubUpdateGateway,
|
||||
UpdateGatewayCause,
|
||||
UpdateGatewayError,
|
||||
)
|
||||
|
||||
Handler = Callable[[httpx.Request], httpx.Response]
|
||||
|
|
@ -33,9 +33,7 @@ async def test_retrieves_latest_version_when_available() -> None:
|
|||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url=GITHUB_API_URL
|
||||
) as client:
|
||||
notifier = GitHubVersionUpdateGateway(
|
||||
"owner", "repo", token="token", client=client
|
||||
)
|
||||
notifier = GitHubUpdateGateway("owner", "repo", token="token", client=client)
|
||||
update = await notifier.fetch_update()
|
||||
|
||||
assert update is not None
|
||||
|
|
@ -54,7 +52,7 @@ async def test_strips_uppercase_prefix_from_tag_name() -> None:
|
|||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url=GITHUB_API_URL
|
||||
) as client:
|
||||
notifier = GitHubVersionUpdateGateway("owner", "repo", client=client)
|
||||
notifier = GitHubUpdateGateway("owner", "repo", client=client)
|
||||
update = await notifier.fetch_update()
|
||||
|
||||
assert update is not None
|
||||
|
|
@ -77,7 +75,7 @@ async def test_considers_no_update_available_when_no_releases_are_found() -> Non
|
|||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url=GITHUB_API_URL
|
||||
) as client:
|
||||
notifier = GitHubVersionUpdateGateway("owner", "repo", client=client)
|
||||
notifier = GitHubUpdateGateway("owner", "repo", client=client)
|
||||
update = await notifier.fetch_update()
|
||||
|
||||
assert update is None
|
||||
|
|
@ -100,7 +98,7 @@ async def test_considers_no_update_available_when_only_drafts_and_prereleases_ar
|
|||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url=GITHUB_API_URL
|
||||
) as client:
|
||||
notifier = GitHubVersionUpdateGateway("owner", "repo", client=client)
|
||||
notifier = GitHubUpdateGateway("owner", "repo", client=client)
|
||||
update = await notifier.fetch_update()
|
||||
|
||||
assert update is None
|
||||
|
|
@ -149,7 +147,7 @@ async def test_picks_the_most_recently_published_non_prerelease_and_non_draft()
|
|||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url=GITHUB_API_URL
|
||||
) as client:
|
||||
notifier = GitHubVersionUpdateGateway("owner", "repo", client=client)
|
||||
notifier = GitHubUpdateGateway("owner", "repo", client=client)
|
||||
update = await notifier.fetch_update()
|
||||
|
||||
assert update is not None
|
||||
|
|
@ -174,7 +172,7 @@ async def test_ignores_draft_releases_and_prereleases(
|
|||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url=GITHUB_API_URL
|
||||
) as client:
|
||||
notifier = GitHubVersionUpdateGateway("owner", "repo", client=client)
|
||||
notifier = GitHubUpdateGateway("owner", "repo", client=client)
|
||||
update = await notifier.fetch_update()
|
||||
|
||||
assert update is None
|
||||
|
|
@ -185,7 +183,7 @@ async def test_ignores_draft_releases_and_prereleases(
|
|||
[
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.NOT_FOUND),
|
||||
VersionUpdateGatewayCause.NOT_FOUND,
|
||||
UpdateGatewayCause.NOT_FOUND,
|
||||
"Unable to fetch the GitHub releases. Did you export a GITHUB_TOKEN environment variable?",
|
||||
),
|
||||
(
|
||||
|
|
@ -193,30 +191,30 @@ async def test_ignores_draft_releases_and_prereleases(
|
|||
status_code=httpx.codes.FORBIDDEN,
|
||||
headers={"X-RateLimit-Remaining": "0"},
|
||||
),
|
||||
VersionUpdateGatewayCause.TOO_MANY_REQUESTS,
|
||||
UpdateGatewayCause.TOO_MANY_REQUESTS,
|
||||
None,
|
||||
),
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.TOO_MANY_REQUESTS),
|
||||
VersionUpdateGatewayCause.TOO_MANY_REQUESTS,
|
||||
UpdateGatewayCause.TOO_MANY_REQUESTS,
|
||||
None,
|
||||
),
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.FORBIDDEN),
|
||||
VersionUpdateGatewayCause.FORBIDDEN,
|
||||
UpdateGatewayCause.FORBIDDEN,
|
||||
None,
|
||||
),
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.INTERNAL_SERVER_ERROR),
|
||||
VersionUpdateGatewayCause.ERROR_RESPONSE,
|
||||
UpdateGatewayCause.ERROR_RESPONSE,
|
||||
None,
|
||||
),
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.OK, text="not json"),
|
||||
VersionUpdateGatewayCause.INVALID_RESPONSE,
|
||||
UpdateGatewayCause.INVALID_RESPONSE,
|
||||
None,
|
||||
),
|
||||
(_raise_connect_timeout, VersionUpdateGatewayCause.REQUEST_FAILED, None),
|
||||
(_raise_connect_timeout, UpdateGatewayCause.REQUEST_FAILED, None),
|
||||
],
|
||||
ids=[
|
||||
"not_found",
|
||||
|
|
@ -231,15 +229,15 @@ async def test_ignores_draft_releases_and_prereleases(
|
|||
@pytest.mark.asyncio
|
||||
async def test_retrieves_nothing_when_fetching_update_fails(
|
||||
handler: Handler,
|
||||
expected_cause: VersionUpdateGatewayCause,
|
||||
expected_cause: UpdateGatewayCause,
|
||||
expected_custom_message: str | None,
|
||||
) -> None:
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url=GITHUB_API_URL
|
||||
) as client:
|
||||
notifier = GitHubVersionUpdateGateway("owner", "repo", client=client)
|
||||
with pytest.raises(VersionUpdateGatewayError) as excinfo:
|
||||
notifier = GitHubUpdateGateway("owner", "repo", client=client)
|
||||
with pytest.raises(UpdateGatewayError) as excinfo:
|
||||
await notifier.fetch_update()
|
||||
|
||||
assert excinfo.value.cause == expected_cause
|
||||
|
|
@ -5,13 +5,11 @@ from collections.abc import Callable
|
|||
import httpx
|
||||
import pytest
|
||||
|
||||
from vibe.cli.update_notifier.adapters.pypi_version_update_gateway import (
|
||||
PyPIVersionUpdateGateway,
|
||||
)
|
||||
from vibe.cli.update_notifier.ports.version_update_gateway import (
|
||||
VersionUpdate,
|
||||
VersionUpdateGatewayCause,
|
||||
VersionUpdateGatewayError,
|
||||
from vibe.cli.update_notifier.adapters.pypi_update_gateway import PyPIUpdateGateway
|
||||
from vibe.cli.update_notifier.ports.update_gateway import (
|
||||
Update,
|
||||
UpdateGatewayCause,
|
||||
UpdateGatewayError,
|
||||
)
|
||||
|
||||
Handler = Callable[[httpx.Request], httpx.Response]
|
||||
|
|
@ -28,7 +26,7 @@ async def test_retrieves_nothing_when_no_versions_are_available() -> None:
|
|||
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport, base_url=PYPI_API_URL) as client:
|
||||
gateway = PyPIVersionUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
gateway = PyPIUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
update = await gateway.fetch_update()
|
||||
|
||||
assert update is None
|
||||
|
|
@ -59,10 +57,10 @@ async def test_retrieves_the_latest_non_yanked_version() -> None:
|
|||
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport, base_url=PYPI_API_URL) as client:
|
||||
gateway = PyPIVersionUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
gateway = PyPIUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
update = await gateway.fetch_update()
|
||||
|
||||
assert update == VersionUpdate(latest_version="1.0.2")
|
||||
assert update == Update(latest_version="1.0.2")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -80,7 +78,7 @@ async def test_retrieves_nothing_when_only_yanked_versions_are_available() -> No
|
|||
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport, base_url=PYPI_API_URL) as client:
|
||||
gateway = PyPIVersionUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
gateway = PyPIUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
update = await gateway.fetch_update()
|
||||
|
||||
assert update is None
|
||||
|
|
@ -104,7 +102,7 @@ async def test_does_not_match_versions_by_substring() -> None:
|
|||
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport, base_url=PYPI_API_URL) as client:
|
||||
gateway = PyPIVersionUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
gateway = PyPIUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
update = await gateway.fetch_update()
|
||||
|
||||
assert update is None
|
||||
|
|
@ -119,37 +117,37 @@ def _raise_connect_timeout(request: httpx.Request) -> httpx.Response:
|
|||
[
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.NOT_FOUND),
|
||||
VersionUpdateGatewayCause.NOT_FOUND,
|
||||
UpdateGatewayCause.NOT_FOUND,
|
||||
None,
|
||||
),
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.FORBIDDEN),
|
||||
VersionUpdateGatewayCause.FORBIDDEN,
|
||||
UpdateGatewayCause.FORBIDDEN,
|
||||
None,
|
||||
),
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.INTERNAL_SERVER_ERROR),
|
||||
VersionUpdateGatewayCause.ERROR_RESPONSE,
|
||||
UpdateGatewayCause.ERROR_RESPONSE,
|
||||
None,
|
||||
),
|
||||
(
|
||||
lambda _: httpx.Response(status_code=httpx.codes.OK, content=b"{not-json"),
|
||||
VersionUpdateGatewayCause.INVALID_RESPONSE,
|
||||
UpdateGatewayCause.INVALID_RESPONSE,
|
||||
None,
|
||||
),
|
||||
(_raise_connect_timeout, VersionUpdateGatewayCause.REQUEST_FAILED, None),
|
||||
(_raise_connect_timeout, UpdateGatewayCause.REQUEST_FAILED, None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieves_nothing_when_fetching_update_fails(
|
||||
handler: Callable[[httpx.Request], httpx.Response],
|
||||
expected_cause: VersionUpdateGatewayCause,
|
||||
expected_cause: UpdateGatewayCause,
|
||||
expected_message: str | None,
|
||||
) -> None:
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport, base_url=PYPI_API_URL) as client:
|
||||
gateway = PyPIVersionUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
with pytest.raises(VersionUpdateGatewayError) as excinfo:
|
||||
gateway = PyPIUpdateGateway(project_name="mistral-vibe", client=client)
|
||||
with pytest.raises(UpdateGatewayError) as excinfo:
|
||||
await gateway.fetch_update()
|
||||
|
||||
assert excinfo.value.cause == expected_cause
|
||||
406
tests/update_notifier/test_ui_update_notification.py
Normal file
406
tests/update_notifier/test_ui_update_notification.py
Normal 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"
|
||||
)
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Protocol
|
||||
|
||||
import pytest
|
||||
from textual.app import Notification
|
||||
|
||||
from tests.update_notifier.adapters.fake_update_cache_repository import (
|
||||
FakeUpdateCacheRepository,
|
||||
)
|
||||
from tests.update_notifier.adapters.fake_version_update_gateway import (
|
||||
FakeVersionUpdateGateway,
|
||||
)
|
||||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.cli.update_notifier import (
|
||||
UpdateCache,
|
||||
VersionUpdate,
|
||||
VersionUpdateGatewayCause,
|
||||
VersionUpdateGatewayError,
|
||||
)
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
from vibe.core.modes import AgentMode
|
||||
|
||||
|
||||
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: FakeVersionUpdateGateway,
|
||||
update_cache_repository: FakeUpdateCacheRepository | None = None,
|
||||
config: VibeConfig | None = None,
|
||||
initial_mode: AgentMode = AgentMode.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: FakeVersionUpdateGateway,
|
||||
update_cache_repository: FakeUpdateCacheRepository
|
||||
| None = update_cache_repository,
|
||||
config: VibeConfig | None = None,
|
||||
initial_mode: AgentMode = AgentMode.DEFAULT,
|
||||
current_version: str = "0.1.0",
|
||||
) -> VibeApp:
|
||||
return VibeApp(
|
||||
config=config or vibe_config_with_update_checks_enabled,
|
||||
initial_mode=initial_mode,
|
||||
version_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 = FakeVersionUpdateGateway(update=VersionUpdate(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\nRun "uv tool upgrade mistral-vibe" to update'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_does_not_display_update_notification_when_not_available(
|
||||
make_vibe_app: VibeAppFactory,
|
||||
) -> None:
|
||||
notifier = FakeVersionUpdateGateway(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 = FakeVersionUpdateGateway(
|
||||
error=VersionUpdateGatewayError(cause=VersionUpdateGatewayCause.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 = FakeVersionUpdateGateway(update=VersionUpdate(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_invoke_gateway_nor_show_update_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 = FakeVersionUpdateGateway(update=VersionUpdate(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 = FakeVersionUpdateGateway(update=VersionUpdate(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 = FakeVersionUpdateGateway(update=VersionUpdate(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\nRun "uv tool upgrade mistral-vibe" to update'
|
||||
)
|
||||
assert notifier.fetch_update_calls == 1
|
||||
|
|
@ -5,19 +5,14 @@ import pytest
|
|||
from tests.update_notifier.adapters.fake_update_cache_repository import (
|
||||
FakeUpdateCacheRepository,
|
||||
)
|
||||
from tests.update_notifier.adapters.fake_version_update_gateway import (
|
||||
FakeVersionUpdateGateway,
|
||||
)
|
||||
from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway
|
||||
from vibe.cli.update_notifier import (
|
||||
Update,
|
||||
UpdateCache,
|
||||
VersionUpdate,
|
||||
VersionUpdateGatewayCause,
|
||||
VersionUpdateGatewayError,
|
||||
)
|
||||
from vibe.cli.update_notifier.version_update import (
|
||||
VersionUpdateError,
|
||||
get_update_if_available,
|
||||
UpdateGatewayCause,
|
||||
UpdateGatewayError,
|
||||
)
|
||||
from vibe.cli.update_notifier.update import UpdateError, get_update_if_available
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -26,14 +21,12 @@ def current_timestamp() -> int:
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieves_the_latest_version_update_when_available() -> None:
|
||||
async def test_retrieves_the_latest_update_when_available() -> None:
|
||||
latest_update = "1.0.3"
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version=latest_update)
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version=latest_update))
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=FakeUpdateCacheRepository(),
|
||||
)
|
||||
|
|
@ -46,12 +39,10 @@ async def test_retrieves_the_latest_version_update_when_available() -> None:
|
|||
async def test_retrieves_nothing_when_the_current_version_is_the_latest() -> None:
|
||||
current_version = "1.0.0"
|
||||
latest_version = "1.0.0"
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version=latest_version)
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version=latest_version))
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version=current_version,
|
||||
update_cache_repository=FakeUpdateCacheRepository(),
|
||||
)
|
||||
|
|
@ -65,12 +56,10 @@ async def test_retrieves_nothing_when_the_current_version_is_greater_than_the_la
|
|||
):
|
||||
current_version = "0.2.0"
|
||||
latest_version = "0.1.2"
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version=latest_version)
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version=latest_version))
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version=current_version,
|
||||
update_cache_repository=FakeUpdateCacheRepository(),
|
||||
)
|
||||
|
|
@ -80,10 +69,10 @@ async def test_retrieves_nothing_when_the_current_version_is_greater_than_the_la
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieves_nothing_when_no_version_is_available() -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(update=None)
|
||||
update_notifier = FakeUpdateGateway(update=None)
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=FakeUpdateCacheRepository(),
|
||||
)
|
||||
|
|
@ -93,12 +82,10 @@ async def test_retrieves_nothing_when_no_version_is_available() -> None:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieves_nothing_when_latest_version_is_invalid() -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version="invalid-version")
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version="invalid-version"))
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=FakeUpdateCacheRepository(),
|
||||
)
|
||||
|
|
@ -110,13 +97,13 @@ async def test_retrieves_nothing_when_latest_version_is_invalid() -> None:
|
|||
async def test_replaces_hyphens_with_plus_signs_in_latest_version_to_conform_with_PEP_440() -> (
|
||||
None
|
||||
):
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update_notifier = FakeUpdateGateway(
|
||||
# if we were not replacing hyphens with plus signs, this should fail for PEP 440
|
||||
update=VersionUpdate(latest_version="1.6.1-jetbrains")
|
||||
update=Update(latest_version="1.6.1-jetbrains")
|
||||
)
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=FakeUpdateCacheRepository(),
|
||||
)
|
||||
|
|
@ -127,12 +114,10 @@ async def test_replaces_hyphens_with_plus_signs_in_latest_version_to_conform_wit
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieves_nothing_when_current_version_is_invalid() -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version="1.0.1")
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version="1.0.1"))
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="invalid-version",
|
||||
update_cache_repository=FakeUpdateCacheRepository(),
|
||||
)
|
||||
|
|
@ -143,27 +128,25 @@ async def test_retrieves_nothing_when_current_version_is_invalid() -> None:
|
|||
@pytest.mark.parametrize(
|
||||
("cause", "expected_message_substring"),
|
||||
[
|
||||
(VersionUpdateGatewayCause.TOO_MANY_REQUESTS, "Rate limit exceeded"),
|
||||
(VersionUpdateGatewayCause.INVALID_RESPONSE, "invalid response"),
|
||||
(UpdateGatewayCause.TOO_MANY_REQUESTS, "Rate limit exceeded"),
|
||||
(UpdateGatewayCause.INVALID_RESPONSE, "invalid response"),
|
||||
(
|
||||
VersionUpdateGatewayCause.NOT_FOUND,
|
||||
UpdateGatewayCause.NOT_FOUND,
|
||||
"Unable to fetch the releases. Please check your permissions.",
|
||||
),
|
||||
(VersionUpdateGatewayCause.ERROR_RESPONSE, "Unexpected response"),
|
||||
(VersionUpdateGatewayCause.REQUEST_FAILED, "Network error"),
|
||||
(UpdateGatewayCause.ERROR_RESPONSE, "Unexpected response"),
|
||||
(UpdateGatewayCause.REQUEST_FAILED, "Network error"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_version_update_error(
|
||||
cause: VersionUpdateGatewayCause, expected_message_substring: str
|
||||
async def test_raises_update_error(
|
||||
cause: UpdateGatewayCause, expected_message_substring: str
|
||||
) -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
error=VersionUpdateGatewayError(cause=cause)
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(error=UpdateGatewayError(cause=cause))
|
||||
|
||||
with pytest.raises(VersionUpdateError) as excinfo:
|
||||
with pytest.raises(UpdateError) as excinfo:
|
||||
await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=FakeUpdateCacheRepository(),
|
||||
)
|
||||
|
|
@ -175,13 +158,11 @@ async def test_raises_version_update_error(
|
|||
async def test_notifies_and_updates_cache_when_repository_is_empty(
|
||||
current_timestamp: int,
|
||||
) -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version="1.0.1")
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version="1.0.1"))
|
||||
update_cache_repository = FakeUpdateCacheRepository()
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=update_cache_repository,
|
||||
get_current_timestamp=lambda: current_timestamp,
|
||||
|
|
@ -190,7 +171,7 @@ async def test_notifies_and_updates_cache_when_repository_is_empty(
|
|||
assert update is not None
|
||||
assert update.latest_version == "1.0.1"
|
||||
assert update.should_notify is True
|
||||
assert version_update_notifier.fetch_update_calls == 1
|
||||
assert update_notifier.fetch_update_calls == 1
|
||||
assert update_cache_repository.update_cache is not None
|
||||
assert update_cache_repository.update_cache.latest_version == "1.0.1"
|
||||
assert update_cache_repository.update_cache.stored_at_timestamp == current_timestamp
|
||||
|
|
@ -200,9 +181,7 @@ async def test_notifies_and_updates_cache_when_repository_is_empty(
|
|||
async def test_does_not_notify_when_an_available_update_has_been_recently_cached(
|
||||
current_timestamp: int,
|
||||
) -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version="1.0.1")
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version="1.0.1"))
|
||||
timestamp_twelve_hours_ago = current_timestamp - 12 * 60 * 60
|
||||
update_cache = UpdateCache(
|
||||
latest_version="1.0.1", stored_at_timestamp=timestamp_twelve_hours_ago
|
||||
|
|
@ -210,7 +189,7 @@ async def test_does_not_notify_when_an_available_update_has_been_recently_cached
|
|||
update_cache_repository = FakeUpdateCacheRepository(update_cache=update_cache)
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=update_cache_repository,
|
||||
get_current_timestamp=lambda: current_timestamp,
|
||||
|
|
@ -219,16 +198,14 @@ async def test_does_not_notify_when_an_available_update_has_been_recently_cached
|
|||
assert update is not None
|
||||
assert update.latest_version == "1.0.1"
|
||||
assert update.should_notify is False
|
||||
assert version_update_notifier.fetch_update_calls == 0
|
||||
assert update_notifier.fetch_update_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieves_nothing_when_the_recently_cached_update_is_the_one_currently_in_use(
|
||||
current_timestamp: int,
|
||||
) -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version="1.0.1")
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version="1.0.1"))
|
||||
timestamp_twelve_hours_ago = current_timestamp - 12 * 60 * 60
|
||||
update_cache = UpdateCache(
|
||||
latest_version="1.0.1", stored_at_timestamp=timestamp_twelve_hours_ago
|
||||
|
|
@ -236,23 +213,21 @@ async def test_retrieves_nothing_when_the_recently_cached_update_is_the_one_curr
|
|||
update_cache_repository = FakeUpdateCacheRepository(update_cache=update_cache)
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.1",
|
||||
update_cache_repository=update_cache_repository,
|
||||
get_current_timestamp=lambda: current_timestamp,
|
||||
)
|
||||
|
||||
assert update is None
|
||||
assert version_update_notifier.fetch_update_calls == 0
|
||||
assert update_notifier.fetch_update_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieves_fresh_update_and_notifies_and_updates_cache_when_cache_is_not_fresh(
|
||||
current_timestamp: int,
|
||||
) -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
update=VersionUpdate(latest_version="1.0.2")
|
||||
)
|
||||
update_notifier = FakeUpdateGateway(update=Update(latest_version="1.0.2"))
|
||||
timestamp_two_days_ago = current_timestamp - 48 * 60 * 60
|
||||
update_cache = UpdateCache(
|
||||
latest_version="1.0.1", stored_at_timestamp=timestamp_two_days_ago
|
||||
|
|
@ -260,14 +235,14 @@ async def test_retrieves_fresh_update_and_notifies_and_updates_cache_when_cache_
|
|||
update_cache_repository = FakeUpdateCacheRepository(update_cache=update_cache)
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=update_cache_repository,
|
||||
get_current_timestamp=lambda: current_timestamp,
|
||||
)
|
||||
|
||||
assert update is not None
|
||||
assert version_update_notifier.fetch_update_calls == 1
|
||||
assert update_notifier.fetch_update_calls == 1
|
||||
assert update.should_notify is True
|
||||
assert update.latest_version == "1.0.2"
|
||||
assert update_cache_repository.update_cache is not None
|
||||
|
|
@ -279,7 +254,7 @@ async def test_retrieves_fresh_update_and_notifies_and_updates_cache_when_cache_
|
|||
async def test_updates_cache_timestamp_with_current_version_when_no_update_is_available(
|
||||
current_timestamp: int,
|
||||
) -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(update=None)
|
||||
update_notifier = FakeUpdateGateway(update=None)
|
||||
timestamp_two_days_ago = current_timestamp - 48 * 60 * 60
|
||||
update_cache = UpdateCache(
|
||||
latest_version="1.0.0", stored_at_timestamp=timestamp_two_days_ago
|
||||
|
|
@ -287,14 +262,14 @@ async def test_updates_cache_timestamp_with_current_version_when_no_update_is_av
|
|||
update_cache_repository = FakeUpdateCacheRepository(update_cache=update_cache)
|
||||
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=update_cache_repository,
|
||||
get_current_timestamp=lambda: current_timestamp,
|
||||
)
|
||||
|
||||
assert update is None
|
||||
assert version_update_notifier.fetch_update_calls == 1
|
||||
assert update_notifier.fetch_update_calls == 1
|
||||
assert update_cache_repository.update_cache is not None
|
||||
assert update_cache_repository.update_cache.latest_version == "1.0.0"
|
||||
assert update_cache_repository.update_cache.stored_at_timestamp == current_timestamp
|
||||
|
|
@ -304,8 +279,8 @@ async def test_updates_cache_timestamp_with_current_version_when_no_update_is_av
|
|||
async def test_updates_cache_timestamp_with_current_version_when_gateway_errors(
|
||||
current_timestamp: int,
|
||||
) -> None:
|
||||
version_update_notifier = FakeVersionUpdateGateway(
|
||||
error=VersionUpdateGatewayError(cause=VersionUpdateGatewayCause.ERROR_RESPONSE)
|
||||
update_notifier = FakeUpdateGateway(
|
||||
error=UpdateGatewayError(cause=UpdateGatewayCause.ERROR_RESPONSE)
|
||||
)
|
||||
timestamp_two_days_ago = current_timestamp - 48 * 60 * 60
|
||||
update_cache = UpdateCache(
|
||||
|
|
@ -313,15 +288,15 @@ async def test_updates_cache_timestamp_with_current_version_when_gateway_errors(
|
|||
)
|
||||
update_cache_repository = FakeUpdateCacheRepository(update_cache=update_cache)
|
||||
|
||||
with pytest.raises(VersionUpdateError):
|
||||
with pytest.raises(UpdateError):
|
||||
await get_update_if_available(
|
||||
version_update_notifier,
|
||||
update_notifier,
|
||||
current_version="1.0.0",
|
||||
update_cache_repository=update_cache_repository,
|
||||
get_current_timestamp=lambda: current_timestamp,
|
||||
)
|
||||
|
||||
assert version_update_notifier.fetch_update_calls == 1
|
||||
assert update_notifier.fetch_update_calls == 1
|
||||
assert update_cache_repository.update_cache is not None
|
||||
assert update_cache_repository.update_cache.latest_version == "1.0.0"
|
||||
assert update_cache_repository.update_cache.stored_at_timestamp == current_timestamp
|
||||
161
tests/update_notifier/test_whats_new.py
Normal file
161
tests/update_notifier/test_whats_new.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.update_notifier.adapters.fake_update_cache_repository import (
|
||||
FakeUpdateCacheRepository,
|
||||
)
|
||||
from vibe.cli.update_notifier import UpdateCache
|
||||
from vibe.cli.update_notifier.whats_new import (
|
||||
load_whats_new_content,
|
||||
mark_version_as_seen,
|
||||
should_show_whats_new,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_show_whats_new_returns_false_when_cache_is_none() -> None:
|
||||
repository = FakeUpdateCacheRepository()
|
||||
|
||||
result = await should_show_whats_new("1.0.0", repository)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_show_whats_new_returns_true_when_seen_whats_new_version_differs() -> (
|
||||
None
|
||||
):
|
||||
cache = UpdateCache(
|
||||
latest_version="1.0.0",
|
||||
stored_at_timestamp=1_700_000_000,
|
||||
seen_whats_new_version="0.9.0",
|
||||
)
|
||||
repository = FakeUpdateCacheRepository(update_cache=cache)
|
||||
|
||||
result = await should_show_whats_new("1.0.0", repository)
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_show_whats_new_returns_false_when_seen_whats_new_version_matches() -> (
|
||||
None
|
||||
):
|
||||
cache = UpdateCache(
|
||||
latest_version="1.0.0",
|
||||
stored_at_timestamp=1_700_000_000,
|
||||
seen_whats_new_version="1.0.0",
|
||||
)
|
||||
repository = FakeUpdateCacheRepository(update_cache=cache)
|
||||
|
||||
result = await should_show_whats_new("1.0.0", repository)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_show_whats_new_returns_true_when_seen_whats_new_version_is_none() -> (
|
||||
None
|
||||
):
|
||||
cache = UpdateCache(
|
||||
latest_version="1.0.0",
|
||||
stored_at_timestamp=1_700_000_000,
|
||||
seen_whats_new_version=None,
|
||||
)
|
||||
repository = FakeUpdateCacheRepository(update_cache=cache)
|
||||
|
||||
result = await should_show_whats_new("1.0.0", repository)
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_load_whats_new_content_returns_none_when_file_does_not_exist(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
|
||||
result = load_whats_new_content()
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_load_whats_new_content_returns_none_when_file_is_empty(tmp_path: Path) -> None:
|
||||
whats_new_file = tmp_path / "whats_new.md"
|
||||
whats_new_file.write_text("")
|
||||
|
||||
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
|
||||
result = load_whats_new_content()
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_load_whats_new_content_returns_none_when_file_contains_only_whitespace(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
whats_new_file = tmp_path / "whats_new.md"
|
||||
whats_new_file.write_text(" \n\t \n ")
|
||||
|
||||
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
|
||||
result = load_whats_new_content()
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_load_whats_new_content_returns_content_when_file_exists(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
whats_new_file = tmp_path / "whats_new.md"
|
||||
content = "# What's New\n\n- Feature 1\n- Feature 2"
|
||||
whats_new_file.write_text(content)
|
||||
|
||||
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
|
||||
result = load_whats_new_content()
|
||||
|
||||
assert result == content
|
||||
|
||||
|
||||
def test_load_whats_new_content_handles_os_error(tmp_path: Path) -> None:
|
||||
whats_new_file = tmp_path / "whats_new.md"
|
||||
whats_new_file.write_text("content")
|
||||
|
||||
with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path):
|
||||
with patch.object(Path, "read_text", side_effect=OSError("Permission denied")):
|
||||
result = load_whats_new_content()
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_version_as_seen_creates_new_cache_when_repository_is_empty() -> (
|
||||
None
|
||||
):
|
||||
repository = FakeUpdateCacheRepository()
|
||||
|
||||
await mark_version_as_seen("1.0.0", repository)
|
||||
|
||||
assert repository.update_cache is not None
|
||||
assert repository.update_cache.latest_version == "1.0.0"
|
||||
assert repository.update_cache.seen_whats_new_version == "1.0.0"
|
||||
assert repository.update_cache.stored_at_timestamp > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_version_as_seen_updates_seen_whats_new_version_preserving_other_fields() -> (
|
||||
None
|
||||
):
|
||||
cache = UpdateCache(
|
||||
latest_version="1.2.0",
|
||||
stored_at_timestamp=1_700_000_000,
|
||||
seen_whats_new_version="1.0.0",
|
||||
)
|
||||
repository = FakeUpdateCacheRepository(update_cache=cache)
|
||||
|
||||
await mark_version_as_seen("1.1.0", repository)
|
||||
|
||||
assert repository.update_cache is not None
|
||||
assert repository.update_cache.latest_version == "1.2.0"
|
||||
assert repository.update_cache.stored_at_timestamp == 1_700_000_000
|
||||
assert repository.update_cache.seen_whats_new_version == "1.1.0"
|
||||
Loading…
Add table
Add a link
Reference in a new issue