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