diff --git a/headroom/cli/__init__.py b/headroom/cli/__init__.py index cc774f527..d4e20bc3c 100644 --- a/headroom/cli/__init__.py +++ b/headroom/cli/__init__.py @@ -1,5 +1,33 @@ -"""Headroom CLI - Command-line interface for memory and proxy management.""" +"""Headroom CLI - Command-line interface for memory and proxy management. +The subcommand submodules are imported eagerly below so they are bound as +attributes of `headroom.cli`. Click registration happens via side effects in +`main.py::_register_commands`, but that only binds them to the *main.py* +module. Tests that do `patch("headroom.cli..")` resolve the target +by walking attributes on the package object, and that lookup fails when a +prior test has popped `headroom.cli` from `sys.modules` and re-imported it +through a path other than `main.py` (e.g. a test that replaces +`sys.modules["headroom.cli.main"]` with a fake to isolate one subcommand). +Doing `from . import ...` here means the submodule attribute binding +survives that kind of sys.modules mutation. +""" + +from . import ( # noqa: F401 + evals, + init, + install, + learn, + mcp, + perf, + proxy, + tools, + wrap, +) from .main import main +try: + from . import memory # noqa: F401 +except ImportError: + pass + __all__ = ["main"] diff --git a/tests/test_cli/test_wrap_copilot.py b/tests/test_cli/test_wrap_copilot.py index 0d948ec32..a18e627a1 100644 --- a/tests/test_cli/test_wrap_copilot.py +++ b/tests/test_cli/test_wrap_copilot.py @@ -2,12 +2,26 @@ from __future__ import annotations +import sys +import types from pathlib import Path from unittest.mock import patch import pytest from click.testing import CliRunner +if "fastapi" not in sys.modules: + fastapi_mod = types.ModuleType("fastapi") + fastapi_mod.FastAPI = type("FastAPI", (), {}) + fastapi_mod.Request = type("Request", (), {}) + fastapi_mod.WebSocket = type("WebSocket", (), {}) + sys.modules["fastapi"] = fastapi_mod + +if "fastapi.responses" not in sys.modules: + responses_mod = types.ModuleType("fastapi.responses") + responses_mod.Response = type("Response", (), {}) + sys.modules["fastapi.responses"] = responses_mod + from headroom.cli import wrap as wrap_cli from headroom.cli.main import main diff --git a/tests/test_proxy_copilot_auth_hooks.py b/tests/test_proxy_copilot_auth_hooks.py index f117e822c..b4558af0b 100644 --- a/tests/test_proxy_copilot_auth_hooks.py +++ b/tests/test_proxy_copilot_auth_hooks.py @@ -11,7 +11,7 @@ import pytest ROOT = Path(__file__).resolve().parents[1] -def _load_handler_module(module_name: str, relative_path: str, monkeypatch: pytest.MonkeyPatch): +def _load_handler_module(monkeypatch: pytest.MonkeyPatch, module_name: str, relative_path: str): proxy_pkg = types.ModuleType("headroom.proxy") proxy_pkg.__path__ = [str(ROOT / "headroom" / "proxy")] monkeypatch.setitem(sys.modules, "headroom.proxy", proxy_pkg) @@ -57,9 +57,9 @@ def _load_handler_module(module_name: str, relative_path: str, monkeypatch: pyte @pytest.mark.asyncio async def test_openai_passthrough_applies_copilot_auth(monkeypatch: pytest.MonkeyPatch) -> None: openai_mod = _load_handler_module( + monkeypatch, "tests.headroom_proxy_handlers_openai", "headroom/proxy/handlers/openai.py", - monkeypatch, ) seen: dict[str, object] = {} @@ -115,9 +115,9 @@ async def test_openai_passthrough_applies_copilot_auth(monkeypatch: pytest.Monke @pytest.mark.asyncio async def test_streaming_response_applies_copilot_auth(monkeypatch: pytest.MonkeyPatch) -> None: streaming_mod = _load_handler_module( + monkeypatch, "tests.headroom_proxy_handlers_streaming", "headroom/proxy/handlers/streaming.py", - monkeypatch, ) seen: dict[str, object] = {}