vibe/tests/browser_sign_in/stubs.py
Clément Drouin f71bfd3b8c
v2.10.1 (#702)
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.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: Val <102326092+vdeva@users.noreply.github.com>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-05-20 11:39:59 +02:00

191 lines
6.3 KiB
Python

from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from urllib.parse import urlencode
from vibe.setup.auth import (
BrowserSignInError,
BrowserSignInErrorCode,
BrowserSignInGateway,
BrowserSignInPollResult,
BrowserSignInProcess,
)
from vibe.setup.auth.browser_sign_in import BrowserSignInService
@dataclass
class ExchangeRequestPayload:
process_id: str
exchange_token: str
code_verifier: str
class StubBrowserSignInGateway(BrowserSignInGateway):
def __init__(
self,
*,
process: BrowserSignInProcess | None = None,
processes: list[BrowserSignInProcess] | None = None,
poll_results: list[BrowserSignInPollResult | BrowserSignInError] | None = None,
exchange_result: str = "sk-browser-key",
exchange_error: BrowserSignInError | None = None,
) -> None:
if process is not None and processes is not None:
msg = "StubBrowserSignInGateway accepts either process or processes."
raise AssertionError(msg)
self._processes = list(processes or ([] if process is None else [process]))
self._poll_results = list(poll_results or [])
self.exchange_result = exchange_result
self.exchange_error = exchange_error
self.code_challenges: list[str] = []
self.polled_urls: list[str] = []
self.exchange_requests: list[ExchangeRequestPayload] = []
self.closed = False
self.poll_calls = 0
self.process_number = 0
async def create_process(self, code_challenge: str) -> BrowserSignInProcess:
self.code_challenges.append(code_challenge)
if not self._processes:
msg = "StubBrowserSignInGateway requires at least one scripted process."
raise AssertionError(msg)
self.process_number += 1
return self._processes.pop(0)
async def poll(self, poll_url: str) -> BrowserSignInPollResult:
self.polled_urls.append(poll_url)
self.poll_calls += 1
if not self._poll_results:
msg = "StubBrowserSignInGateway requires scripted poll results."
raise AssertionError(msg)
result = self._poll_results.pop(0)
if isinstance(result, BrowserSignInError):
raise result
return result
async def exchange(
self, process_id: str, exchange_token: str, code_verifier: str
) -> str:
self.exchange_requests.append(
ExchangeRequestPayload(
process_id=process_id,
exchange_token=exchange_token,
code_verifier=code_verifier,
)
)
if self.exchange_error is not None:
raise self.exchange_error
return self.exchange_result
async def aclose(self) -> None:
self.closed = True
def build_sign_in_process(
now: datetime, process_id: str = "process-1"
) -> BrowserSignInProcess:
fragment = urlencode({
"process_id": process_id,
"complete_token": f"complete-token-{process_id}",
"state": f"state-{process_id}",
})
return BrowserSignInProcess(
process_id=process_id,
sign_in_url=(
f"https://console.mistral.ai/codestral/cli/authenticate#{fragment}"
),
poll_url=(
f"https://console.mistral.ai/api/vibe/sign-in/poll/poll-token-{process_id}"
),
expires_at=now + timedelta(minutes=5),
)
def build_poll_failed_error() -> BrowserSignInError:
return BrowserSignInError(
"Browser sign-in status could not be retrieved.",
code=BrowserSignInErrorCode.POLL_FAILED,
)
def build_poll_results_from_outcomes(
outcomes: list[str],
) -> tuple[
list[BrowserSignInProcess], list[BrowserSignInPollResult | BrowserSignInError]
]:
processes: list[BrowserSignInProcess] = []
poll_results: list[BrowserSignInPollResult | BrowserSignInError] = []
now = datetime(2026, 3, 16, tzinfo=UTC)
for process_index, outcome in enumerate(outcomes, start=1):
process_id = f"process-{process_index}"
processes.append(build_sign_in_process(now, process_id=process_id))
match outcome:
case "completed":
poll_results.extend([
BrowserSignInPollResult(status="pending"),
BrowserSignInPollResult(
status="completed", exchange_token=f"exchange-{process_id}"
),
])
case "expired":
poll_results.append(BrowserSignInPollResult(status="expired"))
case "poll_failed":
poll_results.extend([
BrowserSignInPollResult(status="pending"),
build_poll_failed_error(),
build_poll_failed_error(),
build_poll_failed_error(),
])
case _:
msg = f"Unsupported browser sign-in outcome: {outcome}"
raise AssertionError(msg)
return processes, poll_results
async def noop_sleep(_: float) -> None:
return None
def build_browser_sign_in_service_factory(
outcomes: list[str],
*,
exchange_result: str = "sk-browser-onboarding-test-key",
open_browser: Callable[[str], bool] | None = None,
sleep: Callable[[float], Awaitable[None]] = noop_sleep,
now: Callable[[], datetime] | None = None,
) -> tuple[
StubBrowserSignInGateway,
Callable[[], BrowserSignInService],
list[BrowserSignInService],
]:
processes, poll_results = build_poll_results_from_outcomes(outcomes)
gateway = StubBrowserSignInGateway(
processes=processes, poll_results=poll_results, exchange_result=exchange_result
)
created_services: list[BrowserSignInService] = []
def build_service() -> BrowserSignInService:
service = BrowserSignInService(
gateway,
open_browser=open_browser or (lambda _: True),
sleep=sleep,
now=now
or (
lambda: (
datetime(2026, 3, 16, tzinfo=UTC)
+ timedelta(seconds=gateway.poll_calls)
)
),
poll_interval=0,
)
created_services.append(service)
return service
return gateway, build_service, created_services