|
| 1 | +"""Tests covering DI fallback behavior for the Anthropic controller.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from collections.abc import AsyncIterator |
| 6 | +from typing import Any |
| 7 | + |
| 8 | +from src.core.app.controllers.anthropic_controller import ( |
| 9 | + AnthropicController, |
| 10 | + get_anthropic_controller, |
| 11 | +) |
| 12 | +from src.core.config.app_config import AppConfig |
| 13 | +from src.core.di.container import ServiceCollection |
| 14 | +from src.core.interfaces.application_state_interface import IApplicationState |
| 15 | +from src.core.interfaces.backend_service_interface import IBackendService |
| 16 | +from src.core.interfaces.command_service_interface import ICommandService |
| 17 | +from src.core.interfaces.request_processor_interface import IRequestProcessor |
| 18 | +from src.core.interfaces.response_processor_interface import ( |
| 19 | + IResponseProcessor, |
| 20 | + ProcessedResponse, |
| 21 | +) |
| 22 | +from src.core.interfaces.session_resolver_interface import ISessionResolver |
| 23 | +from src.core.interfaces.session_service_interface import ISessionService |
| 24 | +from src.core.interfaces.wire_capture_interface import IWireCapture |
| 25 | +from src.core.services.application_state_service import ApplicationStateService |
| 26 | +from src.core.services.response_manager_service import AgentResponseFormatter |
| 27 | +from src.core.services.session_resolver_service import DefaultSessionResolver |
| 28 | +from src.core.services.session_service_impl import SessionService |
| 29 | +from src.core.domain.processed_result import ProcessedResult |
| 30 | +from src.core.domain.responses import ResponseEnvelope, StreamingResponseEnvelope |
| 31 | +from src.core.domain.chat import ChatRequest |
| 32 | +from src.core.domain.request_context import RequestContext |
| 33 | +from src.core.repositories.in_memory_session_repository import InMemorySessionRepository |
| 34 | + |
| 35 | + |
| 36 | +class _StubCommandService(ICommandService): |
| 37 | + async def process_commands( |
| 38 | + self, messages: list[Any], session_id: str |
| 39 | + ) -> ProcessedResult: |
| 40 | + return ProcessedResult( |
| 41 | + modified_messages=messages, |
| 42 | + command_executed=False, |
| 43 | + command_results=[], |
| 44 | + ) |
| 45 | + |
| 46 | + |
| 47 | +class _StubBackendService(IBackendService): |
| 48 | + async def call_completion( |
| 49 | + self, |
| 50 | + request: ChatRequest, |
| 51 | + stream: bool = False, |
| 52 | + allow_failover: bool = True, |
| 53 | + context: RequestContext | None = None, |
| 54 | + ) -> ResponseEnvelope | StreamingResponseEnvelope: |
| 55 | + if stream: |
| 56 | + async def _stream() -> AsyncIterator[StreamingResponseEnvelope]: |
| 57 | + yield StreamingResponseEnvelope(content={}, headers={}, status_code=200) |
| 58 | + |
| 59 | + return _stream() |
| 60 | + |
| 61 | + return ResponseEnvelope(content={}, headers={}, status_code=200) |
| 62 | + |
| 63 | + async def validate_backend_and_model( |
| 64 | + self, backend: str, model: str |
| 65 | + ) -> tuple[bool, str | None]: |
| 66 | + return True, None |
| 67 | + |
| 68 | + |
| 69 | +class _StubResponseProcessor(IResponseProcessor): |
| 70 | + async def process_response( |
| 71 | + self, |
| 72 | + response: Any, |
| 73 | + session_id: str, |
| 74 | + context: dict[str, Any] | None = None, |
| 75 | + ) -> ProcessedResponse: |
| 76 | + return ProcessedResponse(content=response) |
| 77 | + |
| 78 | + def process_streaming_response( |
| 79 | + self, response_iterator: AsyncIterator[Any], session_id: str |
| 80 | + ) -> AsyncIterator[ProcessedResponse]: |
| 81 | + async def _generator() -> AsyncIterator[ProcessedResponse]: |
| 82 | + async for chunk in response_iterator: |
| 83 | + yield ProcessedResponse(content=chunk) |
| 84 | + |
| 85 | + return _generator() |
| 86 | + |
| 87 | + async def register_middleware( |
| 88 | + self, middleware: Any, priority: int = 0 |
| 89 | + ) -> None: |
| 90 | + return None |
| 91 | + |
| 92 | + |
| 93 | +class _StubWireCapture(IWireCapture): |
| 94 | + def enabled(self) -> bool: |
| 95 | + return False |
| 96 | + |
| 97 | + async def capture_outbound_request( |
| 98 | + self, |
| 99 | + *, |
| 100 | + context: RequestContext | None, |
| 101 | + session_id: str | None, |
| 102 | + backend: str, |
| 103 | + model: str, |
| 104 | + key_name: str | None, |
| 105 | + request_payload: Any, |
| 106 | + ) -> None: |
| 107 | + return None |
| 108 | + |
| 109 | + async def capture_inbound_response( |
| 110 | + self, |
| 111 | + *, |
| 112 | + context: RequestContext | None, |
| 113 | + session_id: str | None, |
| 114 | + backend: str, |
| 115 | + model: str, |
| 116 | + key_name: str | None, |
| 117 | + response_content: Any, |
| 118 | + ) -> None: |
| 119 | + return None |
| 120 | + |
| 121 | + def wrap_inbound_stream( |
| 122 | + self, |
| 123 | + *, |
| 124 | + context: RequestContext | None, |
| 125 | + session_id: str | None, |
| 126 | + backend: str, |
| 127 | + model: str, |
| 128 | + key_name: str | None, |
| 129 | + stream: AsyncIterator[bytes], |
| 130 | + ) -> AsyncIterator[bytes]: |
| 131 | + return stream |
| 132 | + |
| 133 | + async def shutdown(self) -> None: |
| 134 | + return None |
| 135 | + |
| 136 | + |
| 137 | +def _build_service_provider_without_request_processor(): |
| 138 | + """Create a service provider missing IRequestProcessor to trigger fallback.""" |
| 139 | + services = ServiceCollection() |
| 140 | + |
| 141 | + app_config = AppConfig() |
| 142 | + services.add_instance(AppConfig, app_config) |
| 143 | + |
| 144 | + command_service = _StubCommandService() |
| 145 | + services.add_instance(_StubCommandService, command_service) |
| 146 | + services.add_instance(ICommandService, command_service) |
| 147 | + |
| 148 | + backend_service = _StubBackendService() |
| 149 | + services.add_instance(_StubBackendService, backend_service) |
| 150 | + services.add_instance(IBackendService, backend_service) |
| 151 | + |
| 152 | + session_service = SessionService(InMemorySessionRepository()) |
| 153 | + services.add_instance(SessionService, session_service) |
| 154 | + services.add_instance(ISessionService, session_service) |
| 155 | + |
| 156 | + response_processor = _StubResponseProcessor() |
| 157 | + services.add_instance(_StubResponseProcessor, response_processor) |
| 158 | + services.add_instance(IResponseProcessor, response_processor) |
| 159 | + |
| 160 | + app_state = ApplicationStateService() |
| 161 | + services.add_instance(ApplicationStateService, app_state) |
| 162 | + services.add_instance(IApplicationState, app_state) |
| 163 | + |
| 164 | + session_resolver = DefaultSessionResolver(app_config) |
| 165 | + services.add_instance(DefaultSessionResolver, session_resolver) |
| 166 | + services.add_instance(ISessionResolver, session_resolver) |
| 167 | + |
| 168 | + agent_formatter = AgentResponseFormatter(session_service=session_service) |
| 169 | + services.add_instance(AgentResponseFormatter, agent_formatter) |
| 170 | + |
| 171 | + # Provide a wire capture implementation to satisfy downstream dependencies. |
| 172 | + wire_capture = _StubWireCapture() |
| 173 | + services.add_instance(_StubWireCapture, wire_capture) |
| 174 | + services.add_instance(IWireCapture, wire_capture) |
| 175 | + |
| 176 | + return services.build_service_provider() |
| 177 | + |
| 178 | + |
| 179 | +def test_fallback_request_processor_receives_app_state(): |
| 180 | + """Ensure fallback construction does not drop required DI-managed state.""" |
| 181 | + provider = _build_service_provider_without_request_processor() |
| 182 | + |
| 183 | + # Sanity check: DI resolution path is indeed missing the request processor. |
| 184 | + assert provider.get_service(IRequestProcessor) is None |
| 185 | + |
| 186 | + controller = get_anthropic_controller(provider) |
| 187 | + assert isinstance(controller, AnthropicController) |
| 188 | + |
| 189 | + app_state = provider.get_required_service(ApplicationStateService) |
| 190 | + # The fallback-constructed request processor must receive application state. |
| 191 | + assert controller._processor._app_state is app_state |
0 commit comments