@@ -27,7 +27,6 @@ def raise_permission_error(self, *args, **kwargs):
2727
2828 monkeypatch .setattr (Path , "write_text" , raise_permission_error )
2929 tracker = AgentTracker (state_dir = str (tmp_path ))
30-
3130 record = tracker .register (
3231 role = "orchestrator" ,
3332 model = "test-model" ,
@@ -44,7 +43,6 @@ def test_sync_returns_structured_error_when_vendor_clone_unavailable(monkeypatch
4443 import orchestrator .ecc_tools_sync as sync_mod
4544
4645 monkeypatch .setattr (sync_mod , "_ensure_cloned" , lambda : False )
47-
4846 result = sync_mod .sync_ecc_tools (force = False )
4947
5048 assert result ["status" ] == "error"
@@ -75,8 +73,28 @@ def _load_orchestrator_module():
7573 return mod
7674
7775
76+ def _make_mock_request ():
77+ """Return a minimal mock of starlette.requests.Request that satisfies
78+ slowapi's get_remote_address key_func (needs request.client.host).
79+
80+ fix(tests): slowapi @limiter.limit decorator requires a Request object in
81+ the function signature. Tests that call the handler directly must supply
82+ one; this helper avoids pulling in starlette's full Request machinery.
83+ """
84+ class _Client :
85+ host = "127.0.0.1"
86+
87+ class _MockRequest :
88+ client = _Client ()
89+ # slowapi also reads scope/headers in some paths; provide safe stubs.
90+ headers : dict = {}
91+ scope : dict = {"type" : "http" }
92+
93+ return _MockRequest ()
94+
95+
7896def test_call_ultrathink_appends_ultrathink_path (monkeypatch ):
79- """Bug fix: call_ultrathink must POST to <base> /ultrathink, not bare base URL.
97+ """Bug fix: call_ultrathink must POST to /ultrathink, not bare base URL.
8098
8199 Before the fix: session.post(ULTRATHINK_ENDPOINT, ...)
82100 After the fix: session.post(ULTRATHINK_ENDPOINT.rstrip('/') + '/ultrathink', ...)
@@ -85,25 +103,19 @@ def test_call_ultrathink_appends_ultrathink_path(monkeypatch):
85103 import aiohttp
86104
87105 orch_mod = _load_orchestrator_module ()
88-
89106 captured_urls = []
90107
91108 class _FakeResp :
92- async def __aenter__ (self ):
93- return self
94- async def __aexit__ (self , * a ):
95- pass
96- async def json (self ):
97- return {"result" : "ok" }
109+ async def __aenter__ (self ): return self
110+ async def __aexit__ (self , * a ): pass
111+ async def json (self ): return {"result" : "ok" }
98112
99113 class _FakeSession :
100114 def post (self , url , ** kwargs ):
101115 captured_urls .append (url )
102116 return _FakeResp ()
103- async def __aenter__ (self ):
104- return self
105- async def __aexit__ (self , * a ):
106- pass
117+ async def __aenter__ (self ): return self
118+ async def __aexit__ (self , * a ): pass
107119
108120 monkeypatch .setattr (orch_mod , "ULTRATHINK_ENDPOINT" , "http://localhost:8001" )
109121 monkeypatch .setattr (aiohttp , "ClientSession" , _FakeSession )
@@ -121,6 +133,10 @@ def test_orchestrate_returns_empty_string_when_all_backends_fail(monkeypatch):
121133
122134 Before the fix: Pydantic raises ValidationError because result:str got None.
123135 After the fix: status='success', result='' returned cleanly.
136+
137+ fix(tests): orchestrate() is decorated with @limiter.limit which requires a
138+ starlette Request as the second positional argument. Pass a mock Request so
139+ slowapi's async_wrapper can extract the client IP without IndexError.
124140 """
125141 import asyncio
126142
@@ -148,7 +164,9 @@ async def setex(self, *a): pass
148164 privacy_critical = False ,
149165 enable_critic = False ,
150166 )
151- resp = asyncio .run (orch_mod .orchestrate (req ))
167+ # fix(tests): pass mock Request as second positional arg to satisfy slowapi
168+ mock_request = _make_mock_request ()
169+ resp = asyncio .run (orch_mod .orchestrate (req , mock_request ))
152170
153171 assert resp .result == "" , f"Expected empty string, got: { resp .result !r} "
154172 assert resp .status == "success"
0 commit comments