|
| 1 | +# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +"""Regression test for issue #1621. |
| 4 | +
|
| 5 | +``gaia connectors`` is a *base* CLI command, but ``keyring`` ships only in the |
| 6 | +``[ui]``/``[api]``/``[dev]`` extras. A bare ``pip install amd-gaia`` reaches |
| 7 | +``import keyring`` deep in ``gaia.connectors.store`` on the list/status/credential |
| 8 | +subcommands and used to die with a raw ``ModuleNotFoundError`` that named neither |
| 9 | +the cause nor the fix. |
| 10 | +
|
| 11 | +``gaia.connectors._keyring`` guards that import and re-raises a ``ConnectorsError`` |
| 12 | +naming the exact install command — CLAUDE.md's "fail loudly / actionable errors" |
| 13 | +rule. The CLI handler (``gaia.connectors.cli.handle``) catches ``ConnectorsError`` |
| 14 | +and prints it to stderr, so the user sees the install hint instead of a traceback. |
| 15 | +
|
| 16 | +The promotion of ``keyring`` to a base dep vs. a dedicated extra is a separate |
| 17 | +maintainer packaging decision; this guard is the defense-in-depth half that holds |
| 18 | +regardless of where the dependency ultimately lives. |
| 19 | +""" |
| 20 | + |
| 21 | +from __future__ import annotations |
| 22 | + |
| 23 | +import builtins |
| 24 | +import importlib |
| 25 | +import sys |
| 26 | + |
| 27 | +import pytest |
| 28 | + |
| 29 | +from gaia.connectors.errors import ConnectorsError |
| 30 | + |
| 31 | + |
| 32 | +def test_keyring_guard_raises_actionable_error(monkeypatch: pytest.MonkeyPatch) -> None: |
| 33 | + """Importing the keyring shim without ``keyring`` installed must raise an |
| 34 | + actionable ``ConnectorsError`` (not a bare ``ModuleNotFoundError``).""" |
| 35 | + real_import = builtins.__import__ |
| 36 | + |
| 37 | + def _blocked_import(name, *args, **kwargs): |
| 38 | + if name == "keyring" or name.startswith("keyring."): |
| 39 | + raise ModuleNotFoundError("No module named 'keyring'") |
| 40 | + return real_import(name, *args, **kwargs) |
| 41 | + |
| 42 | + # Drop any cached keyring + shim modules so the guarded import re-runs. |
| 43 | + for mod in list(sys.modules): |
| 44 | + if ( |
| 45 | + mod == "keyring" |
| 46 | + or mod.startswith("keyring.") |
| 47 | + or mod == "gaia.connectors._keyring" |
| 48 | + ): |
| 49 | + monkeypatch.delitem(sys.modules, mod, raising=False) |
| 50 | + |
| 51 | + monkeypatch.setattr(builtins, "__import__", _blocked_import) |
| 52 | + |
| 53 | + with pytest.raises(ConnectorsError) as excinfo: |
| 54 | + importlib.import_module("gaia.connectors._keyring") |
| 55 | + |
| 56 | + msg = str(excinfo.value) |
| 57 | + assert "keyring" in msg |
| 58 | + assert "pip install" in msg |
| 59 | + |
| 60 | + # Restore a clean shim for the rest of the suite (keyring is available again |
| 61 | + # now that the monkeypatch is being torn down at function exit). |
| 62 | + sys.modules.pop("gaia.connectors._keyring", None) |
| 63 | + |
| 64 | + |
| 65 | +def test_keyring_shim_reexports_real_module() -> None: |
| 66 | + """When ``keyring`` IS installed the shim re-exports the real module, |
| 67 | + so ``store``/``mcp_server`` keep using ``keyring`` / ``keyring.errors``.""" |
| 68 | + shim = importlib.import_module("gaia.connectors._keyring") |
| 69 | + real = importlib.import_module("keyring") |
| 70 | + assert shim.keyring is real |
| 71 | + assert hasattr(shim.keyring, "errors") |
0 commit comments