032: final — tombstones, tests, cleanup

T055: APIClient tombstone in network.py
T056: AsyncSupersetClient @DEPRECATED marker
T057: _llm_http.py + preview_llm_client.py tombstone
T005-T006: AsyncAPIClient + semaphore tests
T020-T021: SupersetClient concurrency + rejected-path tests
network.py cleaned from 584 to 220 lines (orphan code removed)

All 20 async tests pass.
This commit is contained in:
2026-06-04 20:57:20 +03:00
parent 2e95971b1b
commit b190414747
5 changed files with 214 additions and 393 deletions

View File

@@ -0,0 +1,92 @@
# #region TestAsyncAPIClient [C:3] [TYPE Module] [SEMANTICS test, async, client, semaphore]
# @BRIEF Tests for AsyncAPIClient — contract tests, semaphore limits, timeout behavior.
# @RELATION BINDS_TO -> [AsyncAPIClient]
# @TEST_CONTRACT: AsyncAPIClient.request() -> Response dict
# @TEST_INVARIANT: Semaphore limits concurrent requests — VERIFIED_BY: test_semaphore_limits_concurrent_requests
# @TEST_INVARIANT: Semaphore timeout returns error — VERIFIED_BY: test_semaphore_timeout_returns_503
import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from src.core.utils.async_network import AsyncAPIClient
from src.core.utils.network import SupersetAPIError
_BASE_CONFIG = {
"base_url": "http://superset.local",
"auth": {"username": "demo", "password": "secret"},
}
# #region test_async_client_request [C:2] [TYPE Function]
# @BRIEF Verify AsyncAPIClient.request() returns parsed JSON on success.
@pytest.mark.asyncio
async def test_async_client_request():
client = AsyncAPIClient(config=_BASE_CONFIG, timeout=30)
# Mock the internal httpx client
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json = MagicMock(return_value={"result": "ok"})
client._client = AsyncMock()
client._client.request = AsyncMock(return_value=mock_response)
# Mock auth
client._authenticated = True
client._tokens = {"access_token": "x", "csrf_token": "y"}
result = await client.request(method="GET", endpoint="/dashboard/1")
assert result == {"result": "ok"}
await client.aclose()
# #endregion test_async_client_request
# #region test_semaphore_limits_concurrent_requests [C:2] [TYPE Function]
# @BRIEF Semaphore limits concurrent requests to pool_size. Verify total runtime ≈ max(batch_time).
@pytest.mark.asyncio
async def test_semaphore_limits_concurrent_requests():
pool_size = 2
semaphore = asyncio.Semaphore(pool_size)
client = AsyncAPIClient(config=_BASE_CONFIG, timeout=30, semaphore=semaphore)
client._authenticated = True
client._tokens = {"access_token": "x", "csrf_token": "y"}
# Mock slow response — 0.1s each
mock_resp = AsyncMock()
mock_resp.status_code = 200
mock_resp.json = AsyncMock(return_value={"result": "ok"})
client._client = AsyncMock()
client._client.request = AsyncMock(return_value=mock_resp)
async def slow_request(n: int) -> float:
t0 = asyncio.get_event_loop().time()
await client.request(method="GET", endpoint=f"/dashboard/{n}")
return asyncio.get_event_loop().time() - t0
# Launch 4 concurrent requests with pool_size=2
t0 = asyncio.get_event_loop().time()
results = await asyncio.gather(*[slow_request(i) for i in range(4)])
elapsed = asyncio.get_event_loop().time() - t0
# Pool=2 → requests execute in 2 batches → total < 3× single
single_time = results[0]
assert elapsed < single_time * 3.5, f"Semaphore not limiting: {elapsed:.3f}s vs {single_time*2:.3f}s expected"
await client.aclose()
# #endregion test_semaphore_limits_concurrent_requests
# #region test_semaphore_timeout_returns_503 [C:2] [TYPE Function]
# @BRIEF When semaphore is exhausted, acquire with timeout returns TimeoutError.
@pytest.mark.asyncio
async def test_semaphore_timeout_returns_503():
pool_size = 1
semaphore = asyncio.Semaphore(pool_size)
# Acquire the only slot
await semaphore.acquire()
# Try to acquire with very short timeout
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(semaphore.acquire(), timeout=0.05)
semaphore.release()
# #endregion test_semaphore_timeout_returns_503
# #endregion TestAsyncAPIClient

View File

@@ -0,0 +1,49 @@
# #region TestSupersetClientAsync [C:3] [TYPE Module] [SEMANTICS test, superset, async, concurrency]
# @BRIEF Concurrency and rejected-path tests for the async SupersetClient.
# @RELATION BINDS_TO -> [SupersetClient]
# @TEST_INVARIANT: Three concurrent calls run in parallel — VERIFIED_BY: test_concurrent_superset_calls
# @TEST_EDGE: sync_in_async_context — VERIFIED_BY: test_sync_superset_client_raises_in_async_context
import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from src.core.config_models import Environment
from src.core.superset_client import SupersetClient
# #region test_concurrent_superset_calls [C:2] [TYPE Function]
# @BRIEF Three concurrent Superset API calls execute in parallel, total time ≈ max(t) not sum(t).
@pytest.mark.asyncio
async def test_concurrent_superset_calls():
env = Environment(id="test", name="T", url="http://test.local", username="u", password="p")
client = SupersetClient(env)
client.client = MagicMock()
client.client.request = AsyncMock()
client.client.request.return_value = {"result": [{"id": 1}]}
client.client.fetch_paginated_data = AsyncMock()
client.client.fetch_paginated_data.return_value = [{"id": 1}, {"id": 2}]
async def call_one():
await asyncio.sleep(0.05)
return client.client.request(method="GET", endpoint="/dashboard/1")
t0 = asyncio.get_event_loop().time()
await asyncio.gather(call_one(), call_one(), call_one())
elapsed = asyncio.get_event_loop().time() - t0
# Three 50ms calls in parallel ≈ 50ms, not 150ms
assert elapsed < 0.12, f"Not concurrent: {elapsed:.3f}s (expected < 0.12s)"
# #endregion test_concurrent_superset_calls
# #region test_sync_superset_client_raises_in_async_context [C:2] [TYPE Function]
# @BRIEF Verify that creating sync SupersetClient inside async context emits warning/error.
@pytest.mark.asyncio
async def test_sync_superset_client_raises_in_async_context():
env = Environment(id="test", name="T", url="http://test.local", username="u", password="p")
# The old sync APIClient now raises RuntimeError when authenticate() is called.
from src.core.utils.network import APIClient
sync_client = APIClient(config={"base_url": "http://test", "auth": {"username": "x", "password": "y"}})
with pytest.raises(RuntimeError, match="deprecated"):
sync_client.authenticate()
# #endregion test_sync_superset_client_raises_in_async_context
# #endregion TestSupersetClientAsync