Co-authored-by: Alexis Tacnet <alexis@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
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: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com>
Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com>
Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-06-04 18:26:35 +02:00 committed by GitHub
parent ad0d5c9520
commit 3f8487f761
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
197 changed files with 10819 additions and 2830 deletions

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
import tomllib
from typing import cast
@ -12,9 +13,12 @@ from textual.geometry import Size
from textual.pilot import Pilot
from textual.screen import Screen
from textual.widget import Widget
from textual.widgets import Input, Link
from textual.widgets import Input, Link, Static
from tests.browser_sign_in.stubs import build_browser_sign_in_service_factory
from tests.browser_sign_in.stubs import (
build_browser_sign_in_service_factory,
build_sign_in_process,
)
from tests.conftest import build_test_vibe_config
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
@ -33,8 +37,10 @@ from vibe.core.types import Backend
from vibe.setup.auth import (
BrowserSignInError,
BrowserSignInErrorCode,
BrowserSignInEvent,
BrowserSignInService,
BrowserSignInStatus,
BrowserSignInStatusChanged,
)
from vibe.setup.auth.api_key_persistence import persist_api_key
import vibe.setup.onboarding as onboarding_module
@ -42,11 +48,19 @@ from vibe.setup.onboarding import OnboardingApp
from vibe.setup.onboarding.context import OnboardingContext
from vibe.setup.onboarding.screens.api_key import ApiKeyScreen
from vibe.setup.onboarding.screens.auth_method import AuthMethodScreen
from vibe.setup.onboarding.screens.browser_sign_in import BrowserSignInScreen
from vibe.setup.onboarding.screens.browser_sign_in import (
SIGN_IN_URL_HELP_PREFIX,
BrowserSignInScreen,
)
from vibe.setup.onboarding.screens.theme_selection import THEMES, ThemeSelectionScreen
CONSOLE_URL = "https://console.mistral.ai"
BROWSER_AUTH_API_URL = "https://console.mistral.ai/api"
TEST_NOW = datetime(2026, 3, 16, tzinfo=UTC)
def _expected_browser_sign_in_url(process_id: str = "process-1") -> str:
return build_sign_in_process(TEST_NOW, process_id=process_id).sign_in_url
async def _wait_for(
@ -94,6 +108,8 @@ def _build_browser_onboarding_app(
*,
browser_sign_in_service_factory: Callable[[], BrowserSignInService] | None = None,
browser_sign_in_success_delay: float = 0,
browser_sign_in_url_help_delay: float = 0,
copy_sign_in_url: Callable[[str], bool] | None = None,
) -> OnboardingApp:
return OnboardingApp(
config=_build_onboarding_config(
@ -102,6 +118,8 @@ def _build_browser_onboarding_app(
),
browser_sign_in_service_factory=browser_sign_in_service_factory,
browser_sign_in_success_delay=browser_sign_in_success_delay,
browser_sign_in_url_help_delay=browser_sign_in_url_help_delay,
copy_sign_in_url=copy_sign_in_url,
)
@ -157,6 +175,14 @@ def _browser_sign_in_step_text(card: Widget) -> str:
return f"{title.render()}\n{detail.render()}"
def _browser_sign_in_hint(screen: Screen) -> str:
return str(screen.query_one("#browser-sign-in-hint", NoMarkupStatic).render())
def _browser_sign_in_url_text(screen: Screen) -> str:
return str(screen.query_one("#browser-sign-in-url", Static).render())
def _build_unexpected_browser_sign_in_service_factory(
outcomes: list[str],
*,
@ -173,11 +199,13 @@ def _build_unexpected_browser_sign_in_service_factory(
self._outcome = outcome
async def authenticate(
self, status_callback: Callable[[BrowserSignInStatus], None] | None = None
self, event_callback: Callable[[BrowserSignInEvent], None] | None = None
) -> str:
if self._outcome == "completed":
if status_callback is not None:
status_callback(BrowserSignInStatus.COMPLETED)
if event_callback is not None:
event_callback(
BrowserSignInStatusChanged(status=BrowserSignInStatus.COMPLETED)
)
return api_key
if self._outcome == "runtime_error":
raise RuntimeError("boom")
@ -328,15 +356,24 @@ async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None:
authenticate_started = asyncio.Event()
finish_authenticate = asyncio.Event()
keep_authenticate_running = asyncio.Event()
copied_urls: list[str] = []
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
class DelayedBrowserSignInService:
async def authenticate(
self, status_callback: Callable[[BrowserSignInStatus], None] | None = None
self, event_callback: Callable[[BrowserSignInEvent], None] | None = None
) -> str:
authenticate_started.set()
await finish_authenticate.wait()
if status_callback is not None:
status_callback(BrowserSignInStatus.OPENING_BROWSER)
if event_callback is not None:
event_callback(
BrowserSignInStatusChanged(
status=BrowserSignInStatus.OPENING_BROWSER
)
)
await keep_authenticate_running.wait()
return "sk-never-reached"
@ -346,7 +383,8 @@ async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None:
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=lambda: cast(
BrowserSignInService, DelayedBrowserSignInService()
)
),
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
@ -358,6 +396,9 @@ async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None:
assert "Open browser" in active_step_text
assert "Getting things ready..." in active_step_text
assert "Browser opened" not in active_step_text
assert _browser_sign_in_url_text(app.screen) == ""
await pilot.press("c")
assert copied_urls == []
finish_authenticate.set()
await _wait_for(
@ -371,6 +412,274 @@ async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None:
)
@pytest.mark.asyncio
async def test_ui_shows_browser_sign_in_url_copy_prompt_without_raw_url() -> None:
blocker = asyncio.Event()
async def wait_forever(_: float) -> None:
await blocker.wait()
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
url_text = _browser_sign_in_url_text(app.screen)
assert "If your browser did not open, copy this URL (press C)" in url_text
assert "process-1" not in url_text
assert _browser_sign_in_hint(app.screen) == (
"Press M to enter API key manually - Esc to cancel"
)
@pytest.mark.asyncio
async def test_ui_delays_browser_sign_in_url_help() -> None:
blocker = asyncio.Event()
async def wait_forever(_: float) -> None:
await blocker.wait()
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
browser_sign_in_url_help_delay=0.3,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: (
"Waiting for authentication..."
in _browser_sign_in_step_text(
_active_browser_sign_in_step_card(app.screen)
)
),
pilot,
)
assert _browser_sign_in_url_text(app.screen) == ""
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
assert "process-1" not in _browser_sign_in_url_text(app.screen)
@pytest.mark.asyncio
async def test_ui_copies_browser_sign_in_url() -> None:
blocker = asyncio.Event()
copied_urls: list[str] = []
async def wait_forever(_: float) -> None:
await blocker.wait()
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
await pilot.press("c")
assert "process-1" not in _browser_sign_in_url_text(app.screen)
assert copied_urls == [_expected_browser_sign_in_url()]
@pytest.mark.asyncio
async def test_ui_copies_browser_sign_in_url_when_help_text_is_clicked() -> None:
blocker = asyncio.Event()
copied_urls: list[str] = []
async def wait_forever(_: float) -> None:
await blocker.wait()
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
url_widget = app.screen.query_one("#browser-sign-in-url", Static)
link_x = url_widget.styles.padding.left + len(SIGN_IN_URL_HELP_PREFIX) + 1
await pilot.click(url_widget, offset=(link_x, 0))
assert copied_urls == [_expected_browser_sign_in_url()]
@pytest.mark.asyncio
async def test_ui_reveals_browser_sign_in_url_when_copy_fails() -> None:
blocker = asyncio.Event()
async def wait_forever(_: float) -> None:
await blocker.wait()
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=lambda _: False,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
assert "process-1" not in _browser_sign_in_url_text(app.screen)
await pilot.press("c")
await _wait_for(
lambda: "process-1" in _browser_sign_in_url_text(app.screen), pilot
)
url_text = _browser_sign_in_url_text(app.screen)
assert "Copy failed. Open this URL manually:" in url_text
@pytest.mark.asyncio
async def test_ui_keeps_last_sign_in_url_copy_prompt_after_open_browser_failure() -> (
None
):
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], open_browser=lambda _: False
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: (
"Failed to open browser for sign-in."
in _browser_sign_in_step_text(
_active_browser_sign_in_step_card(app.screen)
)
),
pilot,
)
assert _browser_sign_in_hint(app.screen) == (
"Press R to retry - Press M to enter API key manually - Esc to cancel"
)
url_text = _browser_sign_in_url_text(app.screen)
assert "If your browser did not open, copy this URL (press C)" in url_text
assert "process-1" not in url_text
@pytest.mark.asyncio
async def test_ui_copies_last_browser_sign_in_url_after_open_browser_failure() -> None:
copied_urls: list[str] = []
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], open_browser=lambda _: False
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
await pilot.press("c")
assert copied_urls == [_expected_browser_sign_in_url()]
@pytest.mark.asyncio
async def test_ui_retry_hides_old_sign_in_url_and_uses_fresh_attempt_url() -> None:
blocker = asyncio.Event()
copied_urls: list[str] = []
async def wait_forever(_: float) -> None:
await blocker.wait()
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["expired", "completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: (
"expired"
in _browser_sign_in_step_text(
_active_browser_sign_in_step_card(app.screen)
)
),
pilot,
)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
await pilot.press("r")
await _wait_for(
lambda: (
"Waiting for authentication..."
in _browser_sign_in_step_text(
_active_browser_sign_in_step_card(app.screen)
)
and "copy this URL" in _browser_sign_in_url_text(app.screen)
),
pilot,
)
assert "process-2" not in _browser_sign_in_url_text(app.screen)
assert "process-1" not in _browser_sign_in_url_text(app.screen)
assert _browser_sign_in_hint(app.screen) == (
"Press M to enter API key manually - Esc to cancel"
)
await pilot.press("c")
assert copied_urls == [_expected_browser_sign_in_url(process_id="process-2")]
@pytest.mark.asyncio
async def test_ui_completes_browser_sign_in_and_retries_after_failure() -> None:
gateway, browser_sign_in_service_factory, created_services = (