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,32 @@
from __future__ import annotations
from vibe.cli.plan_offer.ports.whoami_gateway import (
WhoAmIGatewayError,
WhoAmIGatewayUnauthorized,
WhoAmIResponse,
)
class FakeWhoAmIGateway:
def __init__(
self,
response: WhoAmIResponse | None = None,
*,
unauthorized: bool = False,
error: bool = False,
) -> None:
self._response = response
self._unauthorized = unauthorized
self._error = error
self.calls: list[str] = []
async def whoami(self, api_key: str) -> WhoAmIResponse:
self.calls.append(api_key)
if self._unauthorized:
raise WhoAmIGatewayUnauthorized()
if self._error:
raise WhoAmIGatewayError()
if self._response is None:
msg = "FakeWhoAmIGateway requires a response when no error is set."
raise RuntimeError(msg)
return self._response

View file

@ -0,0 +1,103 @@
from __future__ import annotations
import logging
import pytest
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
from vibe.cli.plan_offer.decide_plan_offer import PlanOfferAction, decide_plan_offer
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
@pytest.mark.asyncio
async def test_proposes_upgrade_without_call_when_api_key_is_empty() -> None:
gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=False,
prompt_switching_to_pro_plan=False,
)
)
action = await decide_plan_offer("", gateway)
assert action is PlanOfferAction.UPGRADE
assert gateway.calls == []
@pytest.mark.parametrize(
("response", "expected"),
[
(
WhoAmIResponse(
is_pro_plan=True,
advertise_pro_plan=False,
prompt_switching_to_pro_plan=False,
),
PlanOfferAction.NONE,
),
(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=True,
prompt_switching_to_pro_plan=False,
),
PlanOfferAction.UPGRADE,
),
(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=False,
prompt_switching_to_pro_plan=True,
),
PlanOfferAction.SWITCH_TO_PRO_KEY,
),
],
ids=["with-a-pro-plan", "without-a-pro-plan", "with-a-non-pro-key"],
)
@pytest.mark.asyncio
async def test_proposes_an_action_based_on_current_plan_status(
response: WhoAmIResponse, expected: PlanOfferAction
) -> None:
gateway = FakeWhoAmIGateway(response)
action = await decide_plan_offer("api-key", gateway)
assert action is expected
assert gateway.calls == ["api-key"]
@pytest.mark.asyncio
async def test_proposes_nothing_when_nothing_is_suggested() -> None:
gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=False,
prompt_switching_to_pro_plan=False,
)
)
action = await decide_plan_offer("api-key", gateway)
assert action is PlanOfferAction.NONE
assert gateway.calls == ["api-key"]
@pytest.mark.asyncio
async def test_proposes_upgrade_when_api_key_is_unauthorized() -> None:
gateway = FakeWhoAmIGateway(unauthorized=True)
action = await decide_plan_offer("bad-key", gateway)
assert action is PlanOfferAction.UPGRADE
assert gateway.calls == ["bad-key"]
@pytest.mark.asyncio
async def test_proposes_none_and_logs_warning_when_gateway_error_occurs(
caplog: pytest.LogCaptureFixture,
) -> None:
gateway = FakeWhoAmIGateway(error=True)
with caplog.at_level(logging.WARNING):
action = await decide_plan_offer("api-key", gateway)
assert action is PlanOfferAction.NONE
assert gateway.calls == ["api-key"]
assert "Failed to fetch plan status." in caplog.text

View file

@ -0,0 +1,122 @@
from __future__ import annotations
import httpx
import pytest
import respx
from vibe.cli.plan_offer.adapters.http_whoami_gateway import HttpWhoAmIGateway
from vibe.cli.plan_offer.ports.whoami_gateway import (
WhoAmIGatewayError,
WhoAmIGatewayUnauthorized,
WhoAmIResponse,
)
@pytest.mark.asyncio
async def test_returns_plan_flags(respx_mock: respx.MockRouter) -> None:
route = respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(
200,
json={
"is_pro_plan": True,
"advertise_pro_plan": False,
"prompt_switching_to_pro_plan": False,
},
)
)
gateway = HttpWhoAmIGateway(base_url="http://test")
response = await gateway.whoami("api-key")
assert route.called
request = route.calls.last.request
assert request.headers["Authorization"] == "Bearer api-key"
assert response.is_pro_plan is True
assert response.advertise_pro_plan is False
assert response.prompt_switching_to_pro_plan is False
@pytest.mark.asyncio
@pytest.mark.parametrize("status_code", [401, 403])
async def test_raises_on_unauthorized(
respx_mock: respx.MockRouter, status_code: int
) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(status_code, json={"error": "unauthorized"})
)
gateway = HttpWhoAmIGateway(base_url="http://test")
with pytest.raises(WhoAmIGatewayUnauthorized):
await gateway.whoami("bad-key")
@pytest.mark.asyncio
async def test_raises_on_non_success(respx_mock: respx.MockRouter) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(500, json={"error": "boom"})
)
gateway = HttpWhoAmIGateway(base_url="http://test")
with pytest.raises(WhoAmIGatewayError):
await gateway.whoami("api-key")
@pytest.mark.asyncio
async def test_incomplete_payload_defaults_missing_flags_to_false(
respx_mock: respx.MockRouter,
) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(200, json={"is_pro_plan": True})
)
gateway = HttpWhoAmIGateway(base_url="http://test")
response = await gateway.whoami("api-key")
assert response == WhoAmIResponse(
is_pro_plan=True, advertise_pro_plan=False, prompt_switching_to_pro_plan=False
)
@pytest.mark.asyncio
async def test_wraps_request_error(respx_mock: respx.MockRouter) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
side_effect=httpx.ConnectError("boom")
)
gateway = HttpWhoAmIGateway(base_url="http://test")
with pytest.raises(WhoAmIGatewayError):
await gateway.whoami("api-key")
@pytest.mark.asyncio
async def test_parses_boolean_strings(respx_mock: respx.MockRouter) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(
200,
json={
"is_pro_plan": "true",
"advertise_pro_plan": "false",
"prompt_switching_to_pro_plan": "true",
},
)
)
gateway = HttpWhoAmIGateway(base_url="http://test")
response = await gateway.whoami("api-key")
assert response == WhoAmIResponse(
is_pro_plan=True, advertise_pro_plan=False, prompt_switching_to_pro_plan=True
)
@pytest.mark.asyncio
async def test_raises_on_invalid_boolean_string(respx_mock: respx.MockRouter) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(200, json={"is_pro_plan": "yes"})
)
gateway = HttpWhoAmIGateway(base_url="http://test")
with pytest.raises(WhoAmIGatewayError):
await gateway.whoami("api-key")

View file

@ -0,0 +1,63 @@
from __future__ import annotations
import pytest
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
from tests.stubs.fake_backend import FakeBackend
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
from vibe.cli.textual_ui.app import VibeApp
from vibe.cli.textual_ui.widgets.messages import PlanOfferMessage
from vibe.core.agent_loop import AgentLoop
from vibe.core.config import SessionLoggingConfig, VibeConfig
def _make_app(gateway: FakeWhoAmIGateway, config: VibeConfig | None = None) -> VibeApp:
config = config or VibeConfig(
session_logging=SessionLoggingConfig(enabled=False), enable_update_checks=False
)
agent_loop = AgentLoop(config=config, backend=FakeBackend())
return VibeApp(agent_loop=agent_loop, plan_offer_gateway=gateway)
@pytest.mark.asyncio
async def test_app_shows_upgrade_offer_in_plan_offer_message(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISTRAL_API_KEY", "api-key")
gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=True,
prompt_switching_to_pro_plan=False,
)
)
app = _make_app(gateway)
async with app.run_test() as pilot:
await pilot.pause(0.1)
offer = app.query_one(PlanOfferMessage)
assert "Upgrade to" in offer.get_text()
assert "Pro" in offer.get_text()
assert gateway.calls == ["api-key"]
@pytest.mark.asyncio
async def test_app_shows_switch_to_pro_key_offer_in_plan_offer_message(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISTRAL_API_KEY", "api-key")
gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=False,
prompt_switching_to_pro_plan=True,
)
)
app = _make_app(gateway)
async with app.run_test() as pilot:
await pilot.pause(0.1)
offer = app.query_one(PlanOfferMessage)
assert "Switch to your" in offer.get_text()
assert "Pro API key" in offer.get_text()
assert gateway.calls == ["api-key"]