refactor(V2): IBKR client — log tickle, type _http, retry once on 401

Code review fixes (commit 0c74691):
- _maybe_tickle logs failures via logger.debug instead of silent pass
- _http typed as httpx.AsyncClient | None
- 30s timeout commented (matches Alpaca, IBKR gateway latency)
- _request retries once on 401 with forced LST refresh (spec §4 IBKR_AUTH_FAILED)
- New tests: test_request_retries_once_on_401, test_request_raises_on_persistent_401

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-03 20:27:37 +00:00
parent 0c74691e7c
commit f4f4e4efd7
2 changed files with 66 additions and 8 deletions
+34
View File
@@ -52,3 +52,37 @@ async def test_get_account_summary(httpx_mock: HTTPXMock, client):
)
data = await client.get_account()
assert "netliquidation" in data
@pytest.mark.asyncio
async def test_request_retries_once_on_401(httpx_mock: HTTPXMock, client):
httpx_mock.add_response(url=re.compile(r".*/tickle"), json={})
# First call returns 401
httpx_mock.add_response(
url=re.compile(r".*/portfolio/DU1234/summary"),
status_code=401, text="session expired",
)
# Second call (after LST refresh) succeeds
httpx_mock.add_response(
url=re.compile(r".*/portfolio/DU1234/summary"),
json={"netliquidation": {"amount": 5000}},
)
data = await client.get_account()
assert data["netliquidation"]["amount"] == 5000
@pytest.mark.asyncio
async def test_request_raises_on_persistent_401(httpx_mock: HTTPXMock, client):
from cerbero_mcp.exchanges.ibkr.oauth import IBKRAuthError
httpx_mock.add_response(url=re.compile(r".*/tickle"), json={})
# Both attempts return 401
httpx_mock.add_response(
url=re.compile(r".*/portfolio/DU1234/summary"),
status_code=401, text="bad creds",
)
httpx_mock.add_response(
url=re.compile(r".*/portfolio/DU1234/summary"),
status_code=401, text="bad creds",
)
with pytest.raises(IBKRAuthError, match="after retry"):
await client.get_account()