vibe/tests/acp/test_authenticate.py
Mathias Gesbert cf3f4ca58f
v2.11.1 (#721)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Mert Unsal <mertunsal1905@gmail.com>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
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: maximevoisin-pm <maxime.voisin@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-05-27 17:10:40 +02:00

387 lines
14 KiB
Python

from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
import pytest
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.acp.exceptions import InternalError, InvalidRequestError
from vibe.core.config import ProviderConfig
from vibe.core.types import Backend
from vibe.setup.auth import (
BrowserSignInAttempt,
BrowserSignInError,
BrowserSignInErrorCode,
)
from vibe.setup.onboarding.context import OnboardingContext
def build_browser_sign_in_attempt(
process_id: str = "process-123",
) -> BrowserSignInAttempt:
return BrowserSignInAttempt(
process_id=process_id,
sign_in_url=f"https://console.mistral.ai/vibe/sign-in/{process_id}",
poll_url=f"https://console.mistral.ai/api/vibe/sign-in/{process_id}",
expires_at=datetime(2026, 4, 23, 12, 0, tzinfo=UTC),
code_verifier="secret-code-verifier",
)
def build_mistral_provider(
*,
api_key_env_var: str = "MISTRAL_API_KEY",
browser_auth_base_url: str = "https://console.mistral.ai",
browser_auth_api_base_url: str = "https://console.mistral.ai/api",
) -> ProviderConfig:
return ProviderConfig(
name="mistral",
api_base="https://api.mistral.ai/v1",
api_key_env_var=api_key_env_var,
browser_auth_base_url=browser_auth_base_url,
browser_auth_api_base_url=browser_auth_api_base_url,
backend=Backend.MISTRAL,
)
def build_unsupported_provider() -> ProviderConfig:
return ProviderConfig(
name="llamacpp",
api_base="http://127.0.0.1:8080/v1",
api_key_env_var="LLAMACPP_API_KEY",
backend=Backend.GENERIC,
)
class MutableOnboardingContextLoader:
def __init__(self, provider: ProviderConfig) -> None:
self.provider = provider
def __call__(self) -> OnboardingContext:
return OnboardingContext(provider=self.provider)
class FakeBrowserSignInService:
def __init__(
self,
*,
attempt: BrowserSignInAttempt | None = None,
api_key: str = "api-key",
authenticate_error: BrowserSignInError | None = None,
start_error: BrowserSignInError | None = None,
complete_errors: list[BrowserSignInError] | None = None,
complete_error: BrowserSignInError | None = None,
) -> None:
self.attempt = attempt or build_browser_sign_in_attempt()
self.api_key = api_key
self.authenticate_error = authenticate_error
self.start_error = start_error
self.complete_errors = list(complete_errors or [])
if complete_error is not None:
self.complete_errors.append(complete_error)
self.close_count = 0
async def authenticate(self) -> str:
if self.authenticate_error is not None:
raise self.authenticate_error
return self.api_key
async def start_attempt(self) -> BrowserSignInAttempt:
if self.start_error is not None:
raise self.start_error
return self.attempt
async def complete_attempt(self, attempt: BrowserSignInAttempt) -> str:
if self.complete_errors:
raise self.complete_errors.pop(0)
if attempt != self.attempt:
raise AssertionError("Unexpected browser sign-in attempt.")
return self.api_key
async def aclose(self) -> None:
self.close_count += 1
class InMemoryApiKeyPersister:
def __init__(self, result: str = "completed") -> None:
self.result = result
self.saved: list[tuple[ProviderConfig, str]] = []
def persist(self, provider: ProviderConfig, api_key: str) -> str:
self.saved.append((provider, api_key))
return self.result
def build_acp_agent(
*,
provider: ProviderConfig | None = None,
browser_sign_in: FakeBrowserSignInService | None = None,
api_key_persister: InMemoryApiKeyPersister | None = None,
) -> tuple[VibeAcpAgentLoop, MutableOnboardingContextLoader, InMemoryApiKeyPersister]:
provider = provider or build_mistral_provider()
browser_sign_in = browser_sign_in or FakeBrowserSignInService()
api_key_persister = api_key_persister or InMemoryApiKeyPersister()
context_loader = MutableOnboardingContextLoader(provider)
return (
VibeAcpAgentLoop(
onboarding_context_loader=context_loader,
browser_sign_in_service_factory=lambda _provider: browser_sign_in,
api_key_persister=api_key_persister.persist,
),
context_loader,
api_key_persister,
)
def require_auth_meta(response: Any, method_id: str) -> dict[str, Any]:
assert response is not None
assert response.field_meta is not None
meta = response.field_meta[method_id]
assert isinstance(meta, dict)
return meta
class TestACPAuthenticate:
@pytest.mark.asyncio
async def test_authenticate_completes_browser_sign_in_and_persists_api_key(
self,
) -> None:
provider = build_mistral_provider()
browser_sign_in = FakeBrowserSignInService(api_key="api-key")
acp_agent_loop, _, api_key_persister = build_acp_agent(
provider=provider, browser_sign_in=browser_sign_in
)
response = await acp_agent_loop.authenticate("browser-auth")
assert require_auth_meta(response, "browser-auth") == {
"persistResult": "completed",
"status": "completed",
}
assert api_key_persister.saved == [(provider, "api-key")]
assert browser_sign_in.close_count == 1
@pytest.mark.asyncio
async def test_authenticate_starts_delegated_browser_sign_in(self) -> None:
attempt = build_browser_sign_in_attempt()
browser_sign_in = FakeBrowserSignInService(attempt=attempt)
acp_agent_loop, _, api_key_persister = build_acp_agent(
browser_sign_in=browser_sign_in
)
response = await acp_agent_loop.authenticate("browser-auth-delegated")
assert require_auth_meta(response, "browser-auth-delegated") == {
"attemptId": "process-123",
"expiresAt": "2026-04-23T12:00:00Z",
"signInUrl": "https://console.mistral.ai/vibe/sign-in/process-123",
}
assert api_key_persister.saved == []
assert browser_sign_in.close_count == 1
@pytest.mark.asyncio
async def test_authenticate_rejects_unsupported_method(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
with pytest.raises(
InvalidRequestError, match="Unsupported auth method: vibe-setup"
):
await acp_agent_loop.authenticate("vibe-setup")
@pytest.mark.asyncio
async def test_authenticate_rejects_browser_sign_in_when_unavailable(self) -> None:
acp_agent_loop = VibeAcpAgentLoop(
onboarding_context_loader=lambda: OnboardingContext(
provider=build_unsupported_provider()
)
)
with pytest.raises(
InvalidRequestError,
match="Browser sign-in is not available for the configured provider.",
):
await acp_agent_loop.authenticate("browser-auth")
@pytest.mark.asyncio
async def test_authenticate_surfaces_start_failures(self) -> None:
browser_sign_in = FakeBrowserSignInService(
authenticate_error=BrowserSignInError(
"Failed to start browser sign-in.",
code=BrowserSignInErrorCode.START_FAILED,
)
)
acp_agent_loop, _, _ = build_acp_agent(browser_sign_in=browser_sign_in)
with pytest.raises(InternalError, match="Failed to start browser sign-in."):
await acp_agent_loop.authenticate("browser-auth")
assert browser_sign_in.close_count == 1
@pytest.mark.asyncio
async def test_authenticate_completes_delegated_browser_sign_in_and_persists_api_key(
self,
) -> None:
provider = build_mistral_provider()
attempt = build_browser_sign_in_attempt()
browser_sign_in = FakeBrowserSignInService(attempt=attempt, api_key="api-key")
acp_agent_loop, _, api_key_persister = build_acp_agent(
provider=provider, browser_sign_in=browser_sign_in
)
start_response = await acp_agent_loop.authenticate("browser-auth-delegated")
attempt_id = require_auth_meta(start_response, "browser-auth-delegated")[
"attemptId"
]
response = await acp_agent_loop.authenticate(
"browser-auth-delegated", action="complete", attemptId=attempt_id
)
assert require_auth_meta(response, "browser-auth-delegated") == {
"attemptId": "process-123",
"persistResult": "completed",
"status": "completed",
}
assert api_key_persister.saved == [(provider, "api-key")]
assert browser_sign_in.close_count == 2
@pytest.mark.asyncio
async def test_authenticate_delegated_completion_uses_provider_captured_at_start(
self,
) -> None:
start_provider = build_mistral_provider()
current_provider = build_mistral_provider(
api_key_env_var="OTHER_API_KEY",
browser_auth_base_url="https://example.com",
browser_auth_api_base_url="https://example.com/api",
)
browser_sign_in = FakeBrowserSignInService(api_key="api-key")
acp_agent_loop, context_loader, api_key_persister = build_acp_agent(
provider=start_provider, browser_sign_in=browser_sign_in
)
start_response = await acp_agent_loop.authenticate("browser-auth-delegated")
attempt_id = require_auth_meta(start_response, "browser-auth-delegated")[
"attemptId"
]
context_loader.provider = current_provider
await acp_agent_loop.authenticate(
"browser-auth-delegated", action="complete", attemptId=attempt_id
)
assert api_key_persister.saved == [(start_provider, "api-key")]
@pytest.mark.asyncio
async def test_authenticate_delegated_completion_uses_started_provider(
self,
) -> None:
provider = build_mistral_provider()
browser_sign_in = FakeBrowserSignInService(api_key="api-key")
acp_agent_loop, context_loader, api_key_persister = build_acp_agent(
provider=provider, browser_sign_in=browser_sign_in
)
start_response = await acp_agent_loop.authenticate("browser-auth-delegated")
attempt_id = require_auth_meta(start_response, "browser-auth-delegated")[
"attemptId"
]
context_loader.provider = build_unsupported_provider()
await acp_agent_loop.authenticate(
"browser-auth-delegated", action="complete", attemptId=attempt_id
)
assert api_key_persister.saved == [(provider, "api-key")]
@pytest.mark.asyncio
async def test_authenticate_delegated_completion_requires_attempt_id(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
with pytest.raises(
InvalidRequestError, match="Missing browser sign-in attempt ID."
):
await acp_agent_loop.authenticate(
"browser-auth-delegated", action="complete"
)
@pytest.mark.asyncio
async def test_authenticate_delegated_completion_rejects_unknown_attempt_id(
self,
) -> None:
acp_agent_loop, _, _ = build_acp_agent()
with pytest.raises(
InvalidRequestError, match="Unknown browser sign-in attempt: process-123"
):
await acp_agent_loop.authenticate(
"browser-auth-delegated", action="complete", attemptId="process-123"
)
@pytest.mark.asyncio
async def test_authenticate_delegated_completion_surfaces_browser_sign_in_failures(
self,
) -> None:
browser_sign_in = FakeBrowserSignInService(
complete_error=BrowserSignInError(
"Browser sign-in timed out.", code=BrowserSignInErrorCode.TIMED_OUT
)
)
acp_agent_loop, _, api_key_persister = build_acp_agent(
browser_sign_in=browser_sign_in
)
start_response = await acp_agent_loop.authenticate("browser-auth-delegated")
attempt_id = require_auth_meta(start_response, "browser-auth-delegated")[
"attemptId"
]
with pytest.raises(InvalidRequestError, match="Browser sign-in timed out."):
await acp_agent_loop.authenticate(
"browser-auth-delegated", action="complete", attemptId=attempt_id
)
assert api_key_persister.saved == []
assert browser_sign_in.close_count == 2
@pytest.mark.asyncio
@pytest.mark.parametrize(
"error_code",
[BrowserSignInErrorCode.EXCHANGE_FAILED, BrowserSignInErrorCode.POLL_FAILED],
)
async def test_authenticate_delegated_completion_keeps_retryable_attempts(
self, error_code: BrowserSignInErrorCode
) -> None:
provider = build_mistral_provider()
browser_sign_in = FakeBrowserSignInService(
api_key="api-key",
complete_errors=[
BrowserSignInError(
"Transient browser sign-in failure.", code=error_code
)
],
)
acp_agent_loop, _, api_key_persister = build_acp_agent(
provider=provider, browser_sign_in=browser_sign_in
)
start_response = await acp_agent_loop.authenticate("browser-auth-delegated")
attempt_id = require_auth_meta(start_response, "browser-auth-delegated")[
"attemptId"
]
with pytest.raises(
InvalidRequestError, match="Transient browser sign-in failure."
):
await acp_agent_loop.authenticate(
"browser-auth-delegated", action="complete", attemptId=attempt_id
)
retry_response = await acp_agent_loop.authenticate(
"browser-auth-delegated", action="complete", attemptId=attempt_id
)
assert require_auth_meta(retry_response, "browser-auth-delegated") == {
"attemptId": attempt_id,
"persistResult": "completed",
"status": "completed",
}
assert api_key_persister.saved == [(provider, "api-key")]
assert browser_sign_in.close_count == 3