diff --git a/aai_cli/core/ws.py b/aai_cli/core/ws.py index 32d11a0c..8156fd46 100644 --- a/aai_cli/core/ws.py +++ b/aai_cli/core/ws.py @@ -50,12 +50,15 @@ def handshake_status(exc: object) -> int | None: ``.response.status_code`` — never the message text. The single classifier for every realtime path (stream, agent, speak), so 401-vs-403 handling can't drift. """ + # Both attributes come off an arbitrary exception via getattr, so they're untyped + # and may be anything (or absent → None); narrow to int before the membership test + # rather than coercing with int(), which would raise on a non-numeric .code. code = getattr(exc, "code", None) - if code in _HANDSHAKE_AUTH_STATUSES: - return int(code) + if isinstance(code, int) and code in _HANDSHAKE_AUTH_STATUSES: + return code status = getattr(getattr(exc, "response", None), "status_code", None) - if status in _HANDSHAKE_AUTH_STATUSES: - return int(status) + if isinstance(status, int) and status in _HANDSHAKE_AUTH_STATUSES: + return status return None diff --git a/tests/test_ws.py b/tests/test_ws.py index ddfe310e..4ffceb50 100644 --- a/tests/test_ws.py +++ b/tests/test_ws.py @@ -58,6 +58,9 @@ def test_handshake_status_reads_both_structured_shapes(): assert handshake_status(RuntimeError("network unreachable")) is None # A WebSocket close code (e.g. 1008 policy violation) is not a handshake status. assert handshake_status(types.SimpleNamespace(code=1008)) is None + # A structured but non-auth HTTP status (e.g. a 500 on the response shape) is not a + # handshake auth status either — only 401/403 qualify, on either shape. + assert handshake_status(_HandshakeRejected(500)) is None def test_is_rejected_key_true_for_handshake_401():