Skip to content

Commit 465b0bc

Browse files
authored
Merge pull request #608 from matdev83/codex/fix-improper-di-usage-in-codebase
Ensure Anthropic fallback request processor keeps app state
2 parents 4caea4b + 0ada648 commit 465b0bc

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

src/core/app/controllers/anthropic_controller.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ def get_anthropic_controller(service_provider: IServiceProvider) -> AnthropicCon
543543
session_manager,
544544
backend_request_manager,
545545
response_manager,
546+
app_state=app_state,
546547
)
547548

548549
# Register it for future use
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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

Comments
 (0)