vibe/tests/cli/test_connector_auth_app.py
Mathias Gesbert 1fd7eea289
v2.9.1 (#644)
Co-authored-by: Brice Carpentier <brice.carpentier@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-04-29 17:20:27 +02:00

368 lines
12 KiB
Python

from __future__ import annotations
from typing import Any, cast
from unittest.mock import MagicMock, patch
import pytest
from textual.widgets import OptionList
from tests.stubs.fake_connector_registry import FakeConnectorRegistry
from vibe.cli.textual_ui.widgets.connector_auth_app import (
ConnectorAuthApp,
_AuthOptionId,
)
from vibe.core.tools.connectors.connector_registry import RemoteTool
def _make_registry(*, with_disconnected: bool = True) -> FakeConnectorRegistry:
connectors: dict[str, list[RemoteTool]] = {
"gmail": [RemoteTool(name="search", description="Search emails")]
}
if with_disconnected:
connectors["slack"] = []
return FakeConnectorRegistry(connectors=connectors)
def _make_app(
connector_name: str = "slack", registry: FakeConnectorRegistry | None = None
) -> ConnectorAuthApp:
reg = registry or _make_registry()
mgr = MagicMock()
return ConnectorAuthApp(
connector_name=connector_name, connector_registry=reg, tool_manager=mgr
)
class TestConnectorAuthAppInit:
def test_widget_id(self) -> None:
app = _make_app()
assert app.id == "connectorauth-app"
def test_stores_connector_name(self) -> None:
app = _make_app(connector_name="gmail")
assert app._connector_name == "gmail"
def test_initial_state(self) -> None:
app = _make_app()
assert app._auth_url is None
assert app._auth_url_visible is False
assert app._status_message is None
class TestAuthUrlFetched:
def test_auth_url_available_shows_menu(self) -> None:
app = _make_app()
option_list = MagicMock()
option_list.get_option_index.return_value = 2
detail = MagicMock()
help_widget = MagicMock()
def query(sel, *a, **kw): # type: ignore[no-untyped-def]
if sel is OptionList:
return option_list
s = str(sel)
if "detail" in s:
return detail
if "help" in s:
return help_widget
return MagicMock()
app.query_one = query # type: ignore[assignment]
app._on_auth_url_fetched("https://auth.example.com/oauth")
assert app._auth_url == "https://auth.example.com/oauth"
assert option_list.clear_options.called
assert option_list.add_option.call_count == 5
option_ids = [
call.args[0].id
for call in option_list.add_option.call_args_list
if hasattr(call.args[0], "id") and call.args[0].id
]
assert _AuthOptionId.OPEN in option_ids
assert _AuthOptionId.COPY in option_ids
assert _AuthOptionId.SHOW in option_ids
def test_auth_url_none_shows_no_auth_message(self) -> None:
app = _make_app()
option_list = MagicMock()
detail = MagicMock()
help_widget = MagicMock()
def query(sel, *a, **kw): # type: ignore[no-untyped-def]
if sel is OptionList:
return option_list
s = str(sel)
if "detail" in s:
return detail
if "help" in s:
return help_widget
return MagicMock()
app.query_one = query # type: ignore[assignment]
app._on_auth_url_fetched(None)
assert app._auth_url is None
last_option = option_list.add_option.call_args_list[-1].args[0]
assert "does not provide authentication" in str(last_option.prompt)
class TestAuthActions:
def test_open_browser_calls_webbrowser(self) -> None:
app = _make_app()
app._auth_url = "https://auth.example.com/oauth"
app.query_one = MagicMock()
with patch("vibe.cli.textual_ui.widgets.connector_auth_app.webbrowser") as wb:
app._open_browser()
wb.open.assert_called_once_with("https://auth.example.com/oauth")
assert app._status_message == "Opened in browser."
def test_open_browser_noop_without_url(self) -> None:
app = _make_app()
app._auth_url = None
with patch("vibe.cli.textual_ui.widgets.connector_auth_app.webbrowser") as wb:
app._open_browser()
wb.open.assert_not_called()
def test_copy_url_calls_clipboard(self) -> None:
app = cast(Any, _make_app())
app._auth_url = "https://auth.example.com/oauth"
with (
patch.object(
type(app), "app", new_callable=lambda: property(lambda s: MagicMock())
),
patch(
"vibe.cli.textual_ui.widgets.connector_auth_app.copy_text_to_clipboard"
) as copy_fn,
):
app._copy_url()
copy_fn.assert_called_once()
assert copy_fn.call_args.args[1] == "https://auth.example.com/oauth"
assert (
copy_fn.call_args.kwargs["success_message"]
== "Auth URL copied to clipboard"
)
def test_copy_url_noop_without_url(self) -> None:
app = _make_app()
app._auth_url = None
with patch(
"vibe.cli.textual_ui.widgets.connector_auth_app.copy_text_to_clipboard"
) as copy_fn:
app._copy_url()
copy_fn.assert_not_called()
def test_toggle_url_shows_then_hides(self) -> None:
app = cast(Any, _make_app())
app._auth_url = "https://auth.example.com/oauth"
detail = MagicMock()
app.query_one = lambda sel, *a, **kw: detail
assert app._auth_url_visible is False
app._toggle_url()
assert app._auth_url_visible is True
text = detail.update.call_args.args[0]
assert "https://auth.example.com/oauth" in text
assert "Once authenticated" in text
app._toggle_url()
assert app._auth_url_visible is False
text = detail.update.call_args.args[0]
assert "https://auth.example.com/oauth" not in text
def test_toggle_url_noop_without_url(self) -> None:
app = _make_app()
app._auth_url = None
app._toggle_url()
assert app._auth_url_visible is False
class TestDetailText:
def test_without_url_visible(self) -> None:
app = cast(Any, _make_app())
app._auth_url = "https://auth.example.com/oauth"
app._auth_url_visible = False
detail = MagicMock()
app.query_one = lambda sel, *a, **kw: detail
app._update_detail_text()
text = detail.update.call_args.args[0]
assert "Once authenticated, press R to refresh" in text
assert "https://auth.example.com" not in text
def test_with_url_visible(self) -> None:
app = cast(Any, _make_app())
app._auth_url = "https://auth.example.com/oauth"
app._auth_url_visible = True
detail = MagicMock()
app.query_one = lambda sel, *a, **kw: detail
app._update_detail_text()
text = detail.update.call_args.args[0]
assert "https://auth.example.com/oauth" in text
assert "Once authenticated, press R to refresh" in text
# URL should come before the footer
url_pos = text.index("https://auth.example.com/oauth")
footer_pos = text.index("Once authenticated")
assert url_pos < footer_pos
class TestCloseAndRefresh:
def test_action_close_posts_message(self) -> None:
app = _make_app()
app.post_message = MagicMock()
app.action_close()
msg = app.post_message.call_args.args[0]
assert isinstance(msg, ConnectorAuthApp.ConnectorAuthClosed)
assert msg.refreshed is False
def test_on_connector_refreshed_with_tools_posts_closed(self) -> None:
app = _make_app()
app.post_message = MagicMock()
app._on_connector_refreshed(3)
msg = app.post_message.call_args.args[0]
assert isinstance(msg, ConnectorAuthApp.ConnectorAuthClosed)
assert msg.refreshed is True
assert msg.connector_name == "slack"
assert "3 tools" in (app._status_message or "")
def test_on_connector_refreshed_without_tools_stays_on_page(self) -> None:
app = _make_app()
app.post_message = MagicMock()
app.query_one = MagicMock()
app._on_connector_refreshed(0)
app.post_message.assert_not_called()
assert app._status_message is not None
assert "pending" in app._status_message
@pytest.mark.asyncio
async def test_action_refresh_dispatches_worker(self) -> None:
app = _make_app()
app.run_worker = MagicMock()
app.query_one = MagicMock()
await app.action_refresh()
assert app._status_message == "Refreshing connector..."
app.run_worker.assert_called_once()
_, kwargs = app.run_worker.call_args
assert kwargs["group"] == "connector_refresh"
class TestOptionSelection:
def test_auth_open_dispatches(self) -> None:
app = _make_app()
app._auth_url = "https://auth.example.com"
app.query_one = MagicMock()
event = MagicMock()
event.option.id = _AuthOptionId.OPEN
with patch("vibe.cli.textual_ui.widgets.connector_auth_app.webbrowser") as wb:
app.on_option_list_option_selected(event)
wb.open.assert_called_once()
def test_auth_copy_dispatches(self) -> None:
app = cast(Any, _make_app())
app._auth_url = "https://auth.example.com"
event = MagicMock()
event.option.id = _AuthOptionId.COPY
with (
patch.object(
type(app), "app", new_callable=lambda: property(lambda s: MagicMock())
),
patch(
"vibe.cli.textual_ui.widgets.connector_auth_app.copy_text_to_clipboard"
) as copy_fn,
):
app.on_option_list_option_selected(event)
copy_fn.assert_called_once()
def test_auth_show_dispatches(self) -> None:
app = cast(Any, _make_app())
app._auth_url = "https://auth.example.com"
app.query_one = MagicMock()
event = MagicMock()
event.option.id = _AuthOptionId.SHOW
app.on_option_list_option_selected(event)
assert app._auth_url_visible is True
def test_unknown_option_is_noop(self) -> None:
app = _make_app()
event = MagicMock()
event.option.id = "something:else"
# Should not raise
app.on_option_list_option_selected(event)
class TestWorkerStateChanged:
def test_auth_url_worker_dispatches(self) -> None:
app = _make_app()
app.query_one = MagicMock()
worker = MagicMock()
worker.group = "auth_url"
worker.is_finished = True
worker.result = "https://auth.example.com"
event = MagicMock()
event.worker = worker
app.on_worker_state_changed(event)
assert app._auth_url == "https://auth.example.com"
def test_connector_refresh_worker_with_tools_posts_closed(self) -> None:
app = _make_app()
app.post_message = MagicMock()
worker = MagicMock()
worker.group = "connector_refresh"
worker.is_finished = True
worker.result = 5
event = MagicMock()
event.worker = worker
app.on_worker_state_changed(event)
msg = app.post_message.call_args.args[0]
assert isinstance(msg, ConnectorAuthApp.ConnectorAuthClosed)
assert msg.refreshed is True
def test_connector_refresh_worker_without_tools_stays(self) -> None:
app = _make_app()
app.post_message = MagicMock()
app.query_one = MagicMock()
worker = MagicMock()
worker.group = "connector_refresh"
worker.is_finished = True
worker.result = 0
event = MagicMock()
event.worker = worker
app.on_worker_state_changed(event)
app.post_message.assert_not_called()
assert "pending" in (app._status_message or "")
def test_unrelated_worker_is_ignored(self) -> None:
app = _make_app()
worker = MagicMock()
worker.group = "other"
worker.is_finished = True
event = MagicMock()
event.worker = worker
# Should not raise
app.on_worker_state_changed(event)