fix(tests): fix stale asyncio.sleep patches and missing tarpit guards in service isolation tests

After the ingester._sleep alias fix, three tests in test_service_isolation.py
still patched `decnet.web.ingester.asyncio.sleep` (the old global-singleton
path). The ingester now calls `_sleep` directly, so those patches no longer
controlled the ingester's sleep — the worker looped with real asyncio.sleep
and the tests hung indefinitely.

Also: four API lifespan tests had no tarpit_watcher_worker patch, letting the
real tarpit task start. And test_api_survives_db_init_failure patched
`decnet.web.api.asyncio.sleep` (the singleton) instead of the existing
`_retry_sleep` alias.

Fixes:
- patch("decnet.web.ingester._sleep", ...) in the three ingester tests
- add tarpit_watcher_worker patch to all four api lifespan tests
- patch("decnet.web.api._retry_sleep", ...) in db_init_failure test
This commit is contained in:
2026-05-10 22:10:54 -04:00
parent ff51ce55e2
commit 52f2f65fa3

View File

@@ -110,7 +110,7 @@ class TestIngesterIsolation:
raise asyncio.CancelledError() raise asyncio.CancelledError()
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": "/tmp/nonexistent-decnet-test.log"}): with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": "/tmp/nonexistent-decnet-test.log"}):
with patch("decnet.web.ingester.asyncio.sleep", side_effect=_controlled_sleep): with patch("decnet.web.ingester._sleep", side_effect=_controlled_sleep):
task = asyncio.create_task(log_ingestion_worker(mock_repo)) task = asyncio.create_task(log_ingestion_worker(mock_repo))
with pytest.raises(asyncio.CancelledError): with pytest.raises(asyncio.CancelledError):
await task await task
@@ -153,7 +153,7 @@ class TestIngesterIsolation:
raise asyncio.CancelledError() raise asyncio.CancelledError()
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": str(tmp_path / "test.log")}): with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": str(tmp_path / "test.log")}):
with patch("decnet.web.ingester.asyncio.sleep", side_effect=_controlled_sleep): with patch("decnet.web.ingester._sleep", side_effect=_controlled_sleep):
task = asyncio.create_task(log_ingestion_worker(mock_repo)) task = asyncio.create_task(log_ingestion_worker(mock_repo))
with pytest.raises(asyncio.CancelledError): with pytest.raises(asyncio.CancelledError):
await task await task
@@ -345,9 +345,11 @@ class TestApiLifespanIsolation:
side_effect=Exception("attacker exploded")): side_effect=Exception("attacker exploded")):
with patch("decnet.sniffer.sniffer_worker", with patch("decnet.sniffer.sniffer_worker",
side_effect=Exception("sniffer exploded")): side_effect=Exception("sniffer exploded")):
# API should still start with patch("decnet.web.api.tarpit_watcher_worker",
async with lifespan(mock_app): return_value=asyncio.sleep(0)):
mock_repo.initialize.assert_awaited_once() # API should still start
async with lifespan(mock_app):
mock_repo.initialize.assert_awaited_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_api_survives_db_init_failure(self): async def test_api_survives_db_init_failure(self):
@@ -359,13 +361,15 @@ class TestApiLifespanIsolation:
mock_repo.initialize = AsyncMock(side_effect=Exception("DB locked")) mock_repo.initialize = AsyncMock(side_effect=Exception("DB locked"))
with patch("decnet.web.api.repo", mock_repo): with patch("decnet.web.api.repo", mock_repo):
with patch("decnet.web.api.asyncio.sleep", new_callable=AsyncMock): with patch("decnet.web.api._retry_sleep", new_callable=AsyncMock):
with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)): with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)):
with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)): with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)):
with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)): with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)):
async with lifespan(mock_app): with patch("decnet.web.api.tarpit_watcher_worker",
# DB init failed 5 times but API is running return_value=asyncio.sleep(0)):
assert mock_repo.initialize.await_count == 5 async with lifespan(mock_app):
# DB init failed 5 times but API is running
assert mock_repo.initialize.await_count == 5
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_api_survives_sniffer_import_failure(self): async def test_api_survives_sniffer_import_failure(self):
@@ -376,22 +380,23 @@ class TestApiLifespanIsolation:
mock_repo = MagicMock() mock_repo = MagicMock()
mock_repo.initialize = AsyncMock() mock_repo.initialize = AsyncMock()
import builtins
real_import = builtins.__import__
def _mock_import(name, *args, **kwargs):
if name == "decnet.sniffer":
raise ImportError("No module named 'scapy'")
return real_import(name, *args, **kwargs)
with patch("decnet.web.api.repo", mock_repo): with patch("decnet.web.api.repo", mock_repo):
with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)): with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)):
with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)): with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)):
with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)): with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)):
# Simulate sniffer import failure with patch("decnet.web.api.tarpit_watcher_worker",
import builtins return_value=asyncio.sleep(0)):
real_import = builtins.__import__ with patch("builtins.__import__", side_effect=_mock_import):
async with lifespan(mock_app):
def _mock_import(name, *args, **kwargs): mock_repo.initialize.assert_awaited_once()
if name == "decnet.sniffer":
raise ImportError("No module named 'scapy'")
return real_import(name, *args, **kwargs)
with patch("builtins.__import__", side_effect=_mock_import):
async with lifespan(mock_app):
mock_repo.initialize.assert_awaited_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_shutdown_handles_already_dead_tasks(self): async def test_shutdown_handles_already_dead_tasks(self):
@@ -410,10 +415,12 @@ class TestApiLifespanIsolation:
with patch("decnet.web.api.log_ingestion_worker", side_effect=_instant_worker): with patch("decnet.web.api.log_ingestion_worker", side_effect=_instant_worker):
with patch("decnet.web.api.log_collector_worker", side_effect=_instant_worker): with patch("decnet.web.api.log_collector_worker", side_effect=_instant_worker):
with patch("decnet.web.api.attacker_profile_worker", side_effect=_instant_worker): with patch("decnet.web.api.attacker_profile_worker", side_effect=_instant_worker):
async with lifespan(mock_app): with patch("decnet.web.api.tarpit_watcher_worker",
# Let tasks finish side_effect=_instant_worker):
await asyncio.sleep(0.05) async with lifespan(mock_app):
# Shutdown should handle already-done tasks gracefully # Let tasks finish
await asyncio.sleep(0.05)
# Shutdown should handle already-done tasks gracefully
# ─── Cross-service cascade tests ──────────────────────────────────────────── # ─── Cross-service cascade tests ────────────────────────────────────────────
@@ -442,7 +449,7 @@ class TestCascadeIsolation:
raise asyncio.CancelledError() raise asyncio.CancelledError()
with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": str(tmp_path / "cascade.log")}): with patch.dict(os.environ, {"DECNET_INGEST_LOG_FILE": str(tmp_path / "cascade.log")}):
with patch("decnet.web.ingester.asyncio.sleep", side_effect=_controlled_sleep): with patch("decnet.web.ingester._sleep", side_effect=_controlled_sleep):
task = asyncio.create_task(log_ingestion_worker(mock_repo)) task = asyncio.create_task(log_ingestion_worker(mock_repo))
with pytest.raises(asyncio.CancelledError): with pytest.raises(asyncio.CancelledError):
await task await task