diff --git a/nanobot/channels/websocket/runtime.py b/nanobot/channels/websocket/runtime.py index 7e54209ef..70bd79a47 100644 --- a/nanobot/channels/websocket/runtime.py +++ b/nanobot/channels/websocket/runtime.py @@ -554,7 +554,7 @@ class WebSocketChannel(BaseChannel): try: return bool(sock.getsockopt(socket.SOL_SOCKET, socket.SO_ACCEPTCONN)) except OSError as exc: - if exc.errno in (errno.ENOPROTOOPT, errno.EOPNOTSUPP, errno.EINVAL): + if exc.errno in (errno.ENOPROTOOPT, errno.EOPNOTSUPP): return True raise diff --git a/tests/channels/test_websocket_listener_health.py b/tests/channels/test_websocket_listener_health.py index 22b85e9c7..2fa0e0eff 100644 --- a/tests/channels/test_websocket_listener_health.py +++ b/tests/channels/test_websocket_listener_health.py @@ -28,6 +28,21 @@ class _StubSocket: return self._value +class _StubServer: + """Minimal server stand-in for the production listener-health boundary.""" + + def __init__(self, sock: _StubSocket, *, serving: bool = True): + self._sock = sock + self._serving = serving + + @property + def sockets(self) -> tuple[_StubSocket, ...]: + return (self._sock,) + + def is_serving(self) -> bool: + return self._serving + + @pytest.fixture def listening_socket() -> socket.socket: sock = socket.socket() @@ -57,7 +72,7 @@ def test_closed_socket_is_not_accepting() -> None: @pytest.mark.parametrize( "unsupported_errno", - [errno.ENOPROTOOPT, errno.EOPNOTSUPP, errno.EINVAL], + [errno.ENOPROTOOPT, errno.EOPNOTSUPP], ) def test_unsupported_sockopt_falls_back_to_fd_liveness(unsupported_errno: int) -> None: """macOS/BSD reject ``SO_ACCEPTCONN`` even on healthy listeners. @@ -70,6 +85,14 @@ def test_unsupported_sockopt_falls_back_to_fd_liveness(unsupported_errno: int) - assert WebSocketChannel._socket_is_accepting(sock) is True +def test_listener_health_uses_unsupported_sockopt_fallback() -> None: + """The fallback must be wired into the health check that controls readiness.""" + sock = _StubSocket(fileno=3, error=OSError(errno.ENOPROTOOPT, "Protocol not available")) + server: Any = _StubServer(sock) + + assert WebSocketChannel._listener_is_serving(server) is True + + def test_unexpected_oserror_propagates() -> None: sock = _StubSocket(fileno=3, error=OSError(errno.EBADF, "Bad file descriptor")) @@ -79,6 +102,14 @@ def test_unexpected_oserror_propagates() -> None: assert excinfo.value.errno == errno.EBADF +def test_listener_health_rejects_invalid_socket_state() -> None: + """``EINVAL`` can mean that a live socket isn't actually listening.""" + sock = _StubSocket(fileno=3, error=OSError(errno.EINVAL, "Invalid argument")) + server: Any = _StubServer(sock) + + assert WebSocketChannel._listener_is_serving(server) is False + + def test_unsupported_sockopt_still_rejects_dead_fd() -> None: """The portability fallback must not mask an already-closed listener.""" sock = _StubSocket(fileno=-1, error=OSError(errno.ENOPROTOOPT, "Protocol not available"))