fix(gateway): prevent scoped lock and resource leaks on connection failure

This commit is contained in:
Ruzzgar
2026-04-19 08:51:34 +03:00
committed by Teknium
parent a5063ff105
commit f23123e7b4
6 changed files with 127 additions and 36 deletions

View File

@@ -91,6 +91,29 @@ class TestSignalAdapterInit:
assert adapter._account_normalized == "+15551234567"
class TestSignalConnectCleanup:
"""Regression coverage for failed connect() cleanup."""
@pytest.mark.asyncio
async def test_releases_lock_and_closes_client_on_healthcheck_failure(self, monkeypatch):
adapter = _make_signal_adapter(monkeypatch)
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=MagicMock(status_code=503))
mock_client.aclose = AsyncMock()
with patch("gateway.platforms.signal.httpx.AsyncClient", return_value=mock_client), \
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \
patch("gateway.status.release_scoped_lock") as mock_release:
result = await adapter.connect()
assert result is False
mock_client.aclose.assert_awaited_once()
mock_release.assert_called_once_with("signal-phone", "+15551234567")
assert adapter.client is None
assert adapter._platform_lock_identity is None
class TestSignalHelpers:
def test_redact_phone_long(self):
from gateway.platforms.helpers import redact_phone

View File

@@ -150,6 +150,31 @@ class TestAppMentionHandler:
assert "/hermes" in registered_commands
class TestSlackConnectCleanup:
"""Regression coverage for failed connect() cleanup."""
@pytest.mark.asyncio
async def test_releases_platform_lock_when_auth_fails(self):
config = PlatformConfig(enabled=True, token="xoxb-fake")
adapter = SlackAdapter(config)
mock_app = MagicMock()
mock_web_client = AsyncMock()
mock_web_client.auth_test = AsyncMock(side_effect=RuntimeError("boom"))
with patch.object(_slack_mod, "AsyncApp", return_value=mock_app), \
patch.object(_slack_mod, "AsyncWebClient", return_value=mock_web_client), \
patch.object(_slack_mod, "AsyncSocketModeHandler", return_value=MagicMock()), \
patch.dict(os.environ, {"SLACK_APP_TOKEN": "xapp-fake"}), \
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \
patch("gateway.status.release_scoped_lock") as mock_release:
result = await adapter.connect()
assert result is False
mock_release.assert_called_once_with("slack-app-token", "xapp-fake")
assert adapter._platform_lock_identity is None
# ---------------------------------------------------------------------------
# TestSendDocument
# ---------------------------------------------------------------------------

View File

@@ -211,6 +211,30 @@ class TestFileHandleClosedOnError:
assert adapter._bridge_log_fh is None
class TestConnectCleanup:
"""Verify failure paths release the scoped session lock."""
@pytest.mark.asyncio
async def test_releases_lock_when_npm_install_fails(self):
adapter = _make_adapter()
def _path_exists(path_obj):
return not str(path_obj).endswith("node_modules")
install_result = MagicMock(returncode=1, stderr="install failed")
with patch("gateway.platforms.whatsapp.check_whatsapp_requirements", return_value=True), \
patch.object(Path, "exists", autospec=True, side_effect=_path_exists), \
patch("subprocess.run", return_value=install_result), \
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \
patch("gateway.status.release_scoped_lock") as mock_release:
result = await adapter.connect()
assert result is False
mock_release.assert_called_once_with("whatsapp-session", str(adapter._session_path))
assert adapter._platform_lock_identity is None
class TestBridgeRuntimeFailure:
"""Verify runtime bridge death is surfaced as a fatal adapter error."""