fix(mattermost): escalate genuine WS auth failures through the fatal-error hook
Follow-up to the salvaged #80489 substring-fallback removal: the structured 401/403 branch still exited with a bare return, leaving _running True — dead listener, healthy-looking is_connected(), gateway never told (the zombie half of the bug, OOF-156 class). It now sets a non-retryable mattermost_auth_error with token guidance and notifies the gateway fatal handler. Also: pytest.importorskip for aiohttp in the verifier probe file (module-level import crashed collection in envs without the optional dep), and probe fixtures updated for the escalation attributes.
This commit is contained in:
parent
684c18b428
commit
f4749a77a5
|
|
@ -753,6 +753,21 @@ class MattermostAdapter(BasePlatformAdapter):
|
|||
import aiohttp
|
||||
if isinstance(exc, aiohttp.WSServerHandshakeError) and exc.status in {401, 403}:
|
||||
logger.error("Mattermost WS auth failed (HTTP %d) — stopping reconnect", exc.status)
|
||||
# Escalate through the fatal-error hook instead of a bare
|
||||
# return: the old silent exit left _running True, so
|
||||
# is_connected() kept reporting healthy while the listener
|
||||
# was dead and the gateway was never told (OOF-156 class).
|
||||
# Type-based only — the substring fallback that used to sit
|
||||
# below this branch misclassified transient errors whose
|
||||
# message merely contained "401" (#80489).
|
||||
self._set_fatal_error(
|
||||
"mattermost_auth_error",
|
||||
f"Mattermost WebSocket authentication rejected (HTTP {exc.status}). "
|
||||
"The bot token is invalid, revoked, or lacks permission — check "
|
||||
"MATTERMOST_TOKEN and the bot account in the System Console.",
|
||||
retryable=False,
|
||||
)
|
||||
await self._notify_fatal_error()
|
||||
return
|
||||
logger.warning("Mattermost WS error: %s — reconnecting in %.0fs", exc, delay)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,11 @@ class TestMattermostWSAuthRetry:
|
|||
"""gateway/platforms/mattermost.py — _ws_loop()"""
|
||||
|
||||
def test_401_handshake_stops_reconnect(self):
|
||||
"""A WSServerHandshakeError with status 401 should stop the loop."""
|
||||
"""A WSServerHandshakeError with status 401 should stop the loop —
|
||||
AND escalate through the fatal-error hook (a bare return used to
|
||||
leave _running True: dead listener, healthy-looking adapter)."""
|
||||
import aiohttp
|
||||
from gateway.config import Platform
|
||||
|
||||
exc = aiohttp.WSServerHandshakeError(
|
||||
request_info=MagicMock(),
|
||||
|
|
@ -34,6 +37,14 @@ class TestMattermostWSAuthRetry:
|
|||
|
||||
adapter = MattermostAdapter.__new__(MattermostAdapter)
|
||||
adapter._closing = False
|
||||
adapter._running = True
|
||||
adapter.platform = Platform.MATTERMOST
|
||||
notified = []
|
||||
|
||||
async def fatal_handler(a):
|
||||
notified.append(a)
|
||||
|
||||
adapter._fatal_error_handler = fatal_handler
|
||||
|
||||
call_count = 0
|
||||
|
||||
|
|
@ -44,10 +55,20 @@ class TestMattermostWSAuthRetry:
|
|||
|
||||
adapter._ws_connect_and_listen = fake_connect
|
||||
|
||||
asyncio.run(adapter._ws_loop())
|
||||
async def run():
|
||||
await adapter._ws_loop()
|
||||
# Let the detached fatal-handler task complete.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
# Should have attempted once and stopped, not retried
|
||||
assert call_count == 1
|
||||
# Escalated: fatal error recorded, _running cleared, handler notified.
|
||||
assert adapter._fatal_error_code == "mattermost_auth_error"
|
||||
assert adapter._fatal_error_retryable is False
|
||||
assert adapter._running is False
|
||||
assert notified == [adapter]
|
||||
|
||||
def test_transient_401_substring_does_not_stop_reconnect(self):
|
||||
"""A transient exception whose stringified message merely contains
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ implementer thought to test:
|
|||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
|
||||
# Optional dep: CI installs [all] extras, but local envs may lack aiohttp —
|
||||
# skip cleanly instead of crashing collection.
|
||||
aiohttp = pytest.importorskip("aiohttp")
|
||||
|
||||
from plugins.platforms.mattermost.adapter import MattermostAdapter
|
||||
|
||||
|
|
@ -33,6 +37,14 @@ from plugins.platforms.mattermost.adapter import MattermostAdapter
|
|||
def _make_adapter(closing: bool = False) -> MattermostAdapter:
|
||||
adapter = MattermostAdapter.__new__(MattermostAdapter)
|
||||
adapter._closing = closing
|
||||
# The genuine-auth path now escalates via _set_fatal_error +
|
||||
# _notify_fatal_error (OOF-156 follow-up), which touch these attributes
|
||||
# that __init__ would normally provide.
|
||||
from gateway.config import Platform
|
||||
|
||||
adapter.platform = Platform.MATTERMOST
|
||||
adapter._running = True
|
||||
adapter._fatal_error_handler = None
|
||||
return adapter
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue